# Plan de Migración: ModuloVentas > **Repositorio:** https://git.norteamericano.cl/Nurfog/rm-ventas.git > **Prompt original:** > necesito migrar este proyecto de .net framework 4.8 a .net core 10 (ya es lts), con conexiones a bases de datos desde mysql a postgresql, ademas que tenga un backend (api).net core 10 c# y un frontend en next.js > los datos de conexion a la base de datos son los siguientes: > host: 192.168.0.254 > puerto: 5432 > usuario: postgres > contraseña: apoca11 > database: ichn > necesito que se cree un plan con los siguientes parametros: > analizar completamente y minuciosamente el proyecto. > los reportes de crystal reports tienen que ser migrados al formato de questpdf > la estrategia de datos sera con entity frameworks para sp simples y ejecutandolos en la base de datos los sp complejos > respecto a los facturadores, no usamos facturachile ni el soap wsAmazonSige > todos los servicios externos los quiero en un proyecto aparte como backend como api, ya que varios proyectos mas lo usan > ademas usar contenedores para backend y otro para frontend > secretos en variables de entorno > crear pruebas integrales e2e > graficamente, el frontend tiene que se identico al original. > todas las estructuras y datos ya se encuentan en la base de datos (nada se migra) --- ## 1. ANÁLISIS DEL PROYECTO ACTUAL ### 1.1 Estructura General ``` /home/juan/dev/ventas/ ├── ModuloVentas.sln # Solución Visual Studio 2022 ├── ModuloVentas.csproj # Consola bootstrap (obsoleto) ├── App.config │ ├── Datos/ # Capa de Datos - Class Library │ ├── Datos.csproj # .NET Framework 4.7.2 │ ├── packages.config # MySql.Data 9.3, MySqlConnector 2.6 │ ├── Conexion.cs # Conexión MySQL (hardcodeada) │ └── 25 clases de entidad: │ Alumno, ArqueoCaja, Comuna, Contrato, Cotizacion, │ CotizacionInscripcion, Curso, Descuento, Diagnostico, │ Documento, Ejecutivo, Empresa, Horario, Informes, │ Jornada, Lead, Programa, Propuesta, Region, Sala, │ SamAntiguo, Samold, Sede, Tarifa, TransbankInfo, Usuario │ ├── Negocio/ # Capa de Negocio - Class Library │ ├── Negocio.csproj # .NET Framework 4.7.2 │ └── 28 clases: │ Nalumno, NArqueoCaja, Ncomuna, Ncontrato, Ncotizacion, │ NcotizacionInscripcion, Ncurso, Ndescuento, Ndiagnostico, │ Ndocumento, Nejecutivo, Nempresa, Nhorario, Ninforme, │ Njornada, Nlead, Nmail, Nprograma, Npropuesta, Nregion, │ Nsala, NsamAntiguo, Nsamold, Nsede, Ntarifa, Nusuario, │ PagadorTransbank, ValidarRut, IchnEncrypt, FechaApi, Mails │ └── Web/ # Presentación - ASP.NET WebForms ├── Web.csproj # .NET Framework 4.7.2 ├── Web.config # Config principal ├── packages.config # 15 paquetes NuGet ├── SAM.Master / SAM.Master.cs # Master Page (sidebar + layout) ├── 40 páginas .aspx + code-behind ├── 17 reportes Crystal Reports (.rpt) ├── 12 Typed DataSets (.xsd) ├── 2 servicios WCF (Connected Services) ├── css/, js/, vendor/, img/ └── Template/, correo/ (HTML emails) ``` ### 1.2 Tecnologías Actuales | Componente | Tecnología | Versión | |-----------|-----------|---------| | **Framework** | .NET Framework | 4.7.2 | | **Frontend** | ASP.NET WebForms + Bootstrap 5 + jQuery | - | | **Base de datos** | MySQL | 9.3 (aws ec2) | | **ORM** | Ninguno (ADO.NET puro) | - | | **Reportes** | Crystal Reports | 13.0.4000 | | **Pagos** | Transbank SDK | 6.0.0 | | **DTE Chile** | LibreDTE (REST) + WSFacturaChile (SOAP) | - | | **ERP/SIGE** | wsAmazonSige (SOAP) | - | | **Auth** | FormsAuthentication + cookies | - | | **Email** | SmtpClient (Gmail SMTP) | - | | **Tests** | Ninguno | - | | **Contenedores** | Ninguno | - | ### 1.3 Bases de Datos Originales (MySQL) | Base de Datos | Uso | |-------------|-----| | `sige_sam_V3` | Principal - leads, cotizaciones, contratos, alumnos | | `sige_sam` | Legado SAM | | `caja_tbk` | Transbank/webpay | | `sige_sam_empresa` | Módulo empresas | ### 1.4 Patrón de Acceso a Datos (Actual) ```csharp // Patrón repetido en las 25 clases de Datos/ private readonly Conexion Conexion = new Conexion(); private MySqlCommand Comando; private MySqlDataAdapter Adaptador; private MySqlDataReader Leer; // Ejemplo típico: Comando = new MySqlCommand("sige_sam_V3.GrabaLead", Conexion.AbrirConnectionMySql()) { CommandType = CommandType.StoredProcedure }; Comando.Parameters.AddWithValue("@param", valor); Adaptador = new MySqlDataAdapter(Comando); Adaptador.Fill(tabla); Conexion.CerrarConnectionMysql(); ``` ### 1.5 Conexiones Actuales (hardcodeadas en Conexion.cs) ```csharp // 4 métodos de conexión, todas hardcodeadas: AbrirConnectionMySql() → sige_sam_V3 @ AWS AbrirConnectionAmazonSamOld()→ sige_sam @ AWS AbrirConnectionTBK() → caja_tbk @ AWS AbrirConnectionMySqlEmp() → sige_sam_empresa @ AWS ``` ### 1.6 Crystal Reports - Inventario Completo | # | Archivo .rpt | Ubicación | Uso en Code-behind | |---|-------------|-----------|-------------------| | 1 | Arqueo.rpt | Plantillas/ | Arqueo.aspx.cs | | 2 | CrystalReportAnexo.rpt | Plantillas/ + Planillas/ | FacturadorBoleta.aspx.cs, pago.aspx.cs | | 3 | CrystalReportContrato.rpt | Plantillas/ + Planillas/ | FacturadorBoleta.aspx.cs, pago.aspx.cs | | 4 | CrystalReportContratoBlack.rpt | Planillas/ | FacturadorBoleta.aspx.cs | | 5 | CrystalReportCotizacion.rpt | Plantillas/ | pago.aspx.cs, Panel.aspx.cs | | 6 | ReportPresupuesto.rpt | Plantillas/ | - | | 7 | CAEMP.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | | 8 | CAEMPSNC.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | | 9 | CAEMPV2.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | | 10 | CC_EMP.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | | 11 | CC_EMPCSD.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | | 12 | CC_PRS.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | | 13 | CC_SNC.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | | 14 | CotizacionCursoCerrado.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | | 15 | CotizacionPlanCentral.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | | 16 | PropuestaComercial.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | | 17 | ReportPresupuesto.rpt | Plantillas/ | - | ### 1.7 Servicios Externos | Servicio | Tipo | Endpoint | Uso | |---------|------|----------|-----| | **wsAmazonSige** | SOAP WCF | `https://api.norteamericano.cl/wsSige/Service.asmx` | Sincronización ventas con ERP/SIGE | | **WSFacturaChile** | SOAP WCF | `http://ws.facturachile.cl/WSServicios.asmx` | Emisión de boletas electrónicas | | **LibreDTE** | REST | `https://libredte.cl` | Emisión DTE (facturación electrónica chilena) | | **Transbank Webpay** | SDK | API Transbank | Pagos online con tarjetas | | **Google OAuth** | OAuth 2.0 | Google APIs | Login con Google | | **SMTP Email** | SMTP | `smtp.gmail.com:587` | Correos transaccionales | --- ## 2. ARQUITECTURA OBJETIVO ``` ┌─────────────────────────────────────────────────────────────┐ │ Frontend (Next.js) │ │ Misma UI exacta: Bootstrap 5 + FontAwesome + CSS original │ │ SAM.Master → Layout.tsx (sidebar idéntica + responsive) │ │ 40 páginas .aspx → 40 rutas en App Router │ │ DataList/GridView → DataTables (react-data-table-component) │ │ Reportes → PDF vía API (QuestPDF) │ │ Auth → JWT + cookies HttpOnly │ └──────────────────────┬──────────────────────────────────────┘ │ HTTP (fetch/axios) ┌──────────────────────▼──────────────────────────────────────┐ │ Backend API (.NET 10) │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Ventas.API (Controllers REST) │ │ │ │ /api/auth, /api/lead, /api/contrato, │ │ │ │ /api/cotizacion, /api/alumno, /api/arqueo, │ │ │ │ /api/curso, /api/descuento, /api/documento, │ │ │ │ /api/ejecutivo, /api/empresa, /api/horario, │ │ │ │ /api/informe, /api/jornada, /api/programa, │ │ │ │ /api/propuesta, /api/region, /api/sala, │ │ │ │ /api/sede, /api/tarifa, /api/usuario │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ ┌──────────────────────▼──────────────────────────────┐ │ │ │ Ventas.Services (Business Logic) │ │ │ │ LeadService, ContratoService, CotizacionService, │ │ │ │ AlumnoService, ArqueoService, UsuarioService, etc. │ │ │ │ EmailService, ReportService (QuestPDF) │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ ┌──────────────────────▼──────────────────────────────┐ │ │ │ Ventas.Infrastructure (Data Layer) │ │ │ │ │ EF Core → SPs simples (FromSql, ExecuteSqlRaw) │ │ │ │ │ Dapper → SPs complejos (multi-resultset) │ │ │ │ │ NpgsqlConnection → PostgreSQL │ │ │ │ └── DbContext, Repositories, UnitOfWork │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ ┌──────────────────────▼──────────────────────────────┐ │ │ │ Ventas.Core (Domain) │ │ │ │ Entidades, DTOs, Interfaces, Enums │ │ │ └─────────────────────────────────────────────────────┘ │ └──────────────────────┬──────────────────────────────────────┘ │ ┌──────────────────────▼──────────────────────────────────────┐ │ PostgreSQL (192.168.0.254:5432) │ │ Database: ichn │ │ Tablas, SPs y datos ya migrados │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ ServicesExternos.API (Proyecto separado) │ │ ├── /api/dte (LibreDTE - emisión DTE Chile) │ │ ├── /api/pagos/transbank (Transbank Webpay Plus) │ │ ├── /api/email (SMTP con MailKit) │ │ ├── /api/auth/google (Google OAuth) │ │ │ │ │ ⚡ Reutilizable por otros proyectos de la organización │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 3. ESTRUCTURA DE DIRECTORIOS (NUEVA) ``` /ventas/ ├── docker-compose.yml ├── .env.example ├── .gitignore │ ├── backend/ │ ├── Ventas.sln │ ├── src/ │ │ ├── Ventas.API/ │ │ │ ├── Controllers/ │ │ │ │ ├── AuthController.cs │ │ │ │ ├── LeadController.cs │ │ │ │ ├── ContratoController.cs │ │ │ │ ├── CotizacionController.cs │ │ │ │ ├── AlumnoController.cs │ │ │ │ ├── ArqueoController.cs │ │ │ │ ├── CursoController.cs │ │ │ │ ├── DescuentoController.cs │ │ │ │ ├── DocumentoController.cs │ │ │ │ ├── EjecutivoController.cs │ │ │ │ ├── EmpresaController.cs │ │ │ │ ├── HorarioController.cs │ │ │ │ ├── InformeController.cs │ │ │ │ ├── JornadaController.cs │ │ │ │ ├── ProgramaController.cs │ │ │ │ ├── PropuestaController.cs │ │ │ │ ├── RegionController.cs │ │ │ │ ├── SalaController.cs │ │ │ │ ├── SedeController.cs │ │ │ │ ├── TarifaController.cs │ │ │ │ └── UsuarioController.cs │ │ │ ├── Middleware/ │ │ │ │ └── JwtMiddleware.cs │ │ │ ├── Program.cs │ │ │ ├── appsettings.json │ │ │ └── Dockerfile │ │ ├── Ventas.Core/ │ │ │ ├── Entities/ │ │ │ │ ├── Lead.cs │ │ │ │ ├── Contrato.cs │ │ │ │ ├── Cotizacion.cs │ │ │ │ ├── Alumno.cs │ │ │ │ ├── ArqueoCaja.cs │ │ │ │ ├── Curso.cs │ │ │ │ ├── Descuento.cs │ │ │ │ ├── Documento.cs │ │ │ │ ├── Ejecutivo.cs │ │ │ │ ├── Empresa.cs │ │ │ │ ├── Horario.cs │ │ │ │ ├── Jornada.cs │ │ │ │ ├── Programa.cs │ │ │ │ ├── Propuesta.cs │ │ │ │ ├── Region.cs │ │ │ │ ├── Sala.cs │ │ │ │ ├── Sede.cs │ │ │ │ ├── Tarifa.cs │ │ │ │ ├── Usuario.cs │ │ │ │ └── Comuna.cs │ │ │ ├── DTOs/ │ │ │ ├── Enums/ │ │ │ └── Interfaces/ │ │ │ ├── ILeadRepository.cs │ │ │ ├── IContratoRepository.cs │ │ │ ├── ICotizacionRepository.cs │ │ │ └── ... │ │ ├── Ventas.Infrastructure/ │ │ │ ├── Data/ │ │ │ │ ├── VentasDbContext.cs │ │ │ │ └── Configurations/ │ │ │ ├── Repositories/ │ │ │ │ ├── LeadRepository.cs │ │ │ │ ├── ContratoRepository.cs │ │ │ │ └── ... │ │ │ └── Dapper/ │ │ │ └── SpComplexQueries.cs │ │ └── Ventas.Services/ │ │ ├── LeadService.cs │ │ ├── ContratoService.cs │ │ ├── CotizacionService.cs │ │ ├── AlumnoService.cs │ │ ├── ArqueoService.cs │ │ ├── UsuarioService.cs │ │ ├── MailsService.cs │ │ ├── ReportService.cs (QuestPDF) │ │ └── ... │ └── tests/ │ ├── Ventas.UnitTests/ │ │ ├── Services/ │ │ └── ... │ └── Ventas.IntegrationTests/ │ ├── Repositories/ │ └── ... │ ├── services-externos/ │ ├── ServicesExternos.sln │ ├── src/ │ │ ├── ServicesExternos.API/ │ │ │ ├── Controllers/ │ │ │ │ ├── DteController.cs │ │ │ │ ├── TransbankController.cs │ │ │ │ ├── EmailController.cs │ │ │ │ └── GoogleAuthController.cs │ │ │ ├── Program.cs │ │ │ └── Dockerfile │ │ ├── ServicesExternos.Core/ │ │ └── ServicesExternos.Infrastructure/ │ └── tests/ │ ├── frontend/ │ ├── next.config.js │ ├── package.json │ ├── tsconfig.json │ ├── Dockerfile │ ├── public/ │ │ ├── img/ │ │ │ ├── favicon.png │ │ │ ├── ichnHD.png │ │ │ ├── Invertido_ICN.png │ │ │ ├── samito.png │ │ │ └── 03.jpg │ │ └── ... │ └── src/ │ ├── app/ │ │ ├── layout.tsx # = SAM.Master (sidebar + header) │ │ ├── page.tsx # redirect a /dashboard │ │ ├── login/ │ │ │ └── page.tsx # = Login.aspx │ │ ├── dashboard/ │ │ │ └── page.tsx # = Index.aspx │ │ ├── leads/ │ │ │ ├── page.tsx # = LeadV2.aspx │ │ │ ├── [id]/ │ │ │ │ └── page.tsx # = LeadPanel.aspx │ │ │ └── nuevo/ │ │ │ └── page.tsx # = Lead.aspx │ │ ├── empresas/ │ │ │ ├── page.tsx # = Empresas.aspx │ │ │ ├── [id]/ │ │ │ │ └── page.tsx # = AdminEmpresa.aspx │ │ │ └── ventas/ │ │ │ └── page.tsx # = VentaEmpresa.aspx │ │ ├── cotizaciones/ │ │ │ └── page.tsx # = pago.aspx │ │ ├── arqueo/ │ │ │ └── page.tsx # = Arqueo.aspx │ │ ├── cursos/ │ │ │ └── page.tsx # = Aperturas.aspx │ │ ├── alumnos/ │ │ │ └── page.tsx # = Ficha.aspx │ │ ├── reportes/ │ │ │ ├── ventas/ │ │ │ │ └── page.tsx # = ResumenVentas.aspx │ │ │ ├── ventas-empresas/ │ │ │ │ └── page.tsx # = ResumenVentasEmp.aspx │ │ │ └── reimpresion/ │ │ │ └── page.tsx # = Reimpresion.aspx │ │ ├── autorizador/ │ │ │ └── page.tsx # = Autorizador.aspx │ │ ├── contacto/ │ │ │ └── page.tsx # = Contacto.aspx │ │ └── pdf/ # PDFs generados por API │ ├── components/ │ │ ├── Sidebar.tsx # Navegación idéntica al original │ │ ├── Header.tsx # Barra superior con usuario │ │ ├── DataTable.tsx # Reemplazo de DataList/GridView │ │ ├── ModalConfirmacion.tsx │ │ ├── LoadingSpinner.tsx │ │ └── ... │ ├── services/ │ │ └── api.ts # Cliente HTTP centralizado │ ├── hooks/ │ │ ├── useAuth.ts │ │ ├── useInactividad.ts # Timeout 30 min (original) │ │ └── ... │ ├── lib/ │ │ ├── validarRut.ts # Migrado de validarRUT.js │ │ └── utils.ts │ └── styles/ │ ├── globals.css │ ├── style.css # Mismo CSS original │ ├── ichn.css # Mismo CSS original │ ├── ichnPaginas.css # Mismo CSS original │ └── checkRadio.css # Mismo CSS original │ └── scripts/ ├── migracion/ # Scripts de referencia └── deploy/ ``` --- ## 4. FASES DE MIGRACIÓN ### FASE 1: BACKEND .NET 10 — API REST (6-8 semanas) #### 1.1 Crear solución y proyectos base ```bash dotnet new sln -n Ventas dotnet new webapi -n Ventas.API dotnet new classlib -n Ventas.Core dotnet new classlib -n Ventas.Infrastructure dotnet new classlib -n Ventas.Services dotnet sln add src/**/*.csproj ``` #### 1.2 Ventas.Core — Entidades y DTOs Mapear todas las entidades desde las 25 clases de `Datos/`. Crear DTOs para request/response de cada endpoint. **Ejemplo Lead (Core):** ```csharp namespace Ventas.Core.Entities; public class Lead { public int Id { get; set; } public string Nombre { get; set; } = string.Empty; public string? Mail { get; set; } public string? Telefono { get; set; } public string? Producto { get; set; } public int? Contacto { get; set; } public int? EjecutivoId { get; set; } public int? Estado { get; set; } public DateTime? FechaCreacion { get; set; } // ... resto de propiedades } ``` #### 1.3 Ventas.Infrastructure — Acceso a Datos **Estrategia EF Core + Dapper:** | Tipo de SP | Estrategia | Ejemplo | |-----------|-----------|---------| | **SP simple** (1 tabla de vuelta) | `EF Core FromSqlRaw` | `Lead_buscarXestado` | | **SP simple** (INSERT/UPDATE sin return) | `EF Core ExecuteSqlRaw` | `GrabaLead` | | **SP complejo** (multi-resultset) | **Dapper** `QueryMultipleAsync` | `InformeVentasAgnoMesEjecutivo` | | **SP complejo** (cursor, temp tables) | **Dapper** + `CommandType.StoredProcedure` | Reportes Crystal | **DbContext (EF Core):** ```csharp public class VentasDbContext : DbContext { public VentasDbContext(DbContextOptions options) : base(options) { } // DbSets solo para entidades que usan FromSqlRaw public DbSet Leads { get; set; } public DbSet Contratos { get; set; } // ... protected override void OnModelCreating(ModelBuilder modelBuilder) { // Configurar entidades sin tabla (solo para SPs) modelBuilder.Entity(e => { e.HasNoKey(); e.ToView(null); }); } } ``` **Repositorio con Dapper (SPs complejos):** ```csharp public class InformeRepository { private readonly string _connectionString; public async Task InformeVentasAgnoMesAsync(int mes, int agno, string tipo) { using var connection = new NpgsqlConnection(_connectionString); using var multi = await connection.QueryMultipleAsync( "sige_sam_v3.InformeVentasAgnoMes", new { mes, agno, tipo }, commandType: CommandType.StoredProcedure ); return new InformeVentasDto { Resumen = multi.ReadSingle(), Detalle = multi.Read().ToList(), Totales = multi.ReadSingle() }; } } ``` **Paquetes NuGet requeridos:** ```xml ``` #### 1.4 Ventas.Services — Lógica de Negocio Migrar las 28 clases de `Negocio/` a servicios con Inyección de Dependencias. ```csharp public class LeadService : ILeadService { private readonly ILeadRepository _leadRepository; public LeadService(ILeadRepository leadRepository) { _leadRepository = leadRepository; } public async Task> IngresarAsync(LeadCreateDto dto) { // Validar datos // Llamar repositorio // Aplicar reglas de negocio } } ``` #### 1.5 Ventas.API — Controladores REST Crear 1 endpoint por cada método relevante de las 25 clases de datos + 40 code-behind. **Ejemplo LeadController:** ```csharp [ApiController] [Route("api/[controller]")] [Authorize] public class LeadController : ControllerBase { private readonly ILeadService _leadService; public LeadController(ILeadService leadService) { _leadService = leadService; } [HttpGet("{id}")] public async Task GetById(int id) { var result = await _leadService.BuscarIdAsync(id); return Ok(result); } [HttpGet("buscar")] public async Task Buscar( [FromQuery] int ejecutivo, [FromQuery] int estado, [FromQuery] int dias) { var result = await _leadService.BuscarAsync(ejecutivo, estado, dias); return Ok(result); } [HttpPost] public async Task Ingresar(LeadCreateDto dto) { var result = await _leadService.IngresarAsync(dto); return Ok(new { mensaje = result }); } [HttpPost("{id}/actividad")] public async Task IngresarActividad(int id, ActividadCreateDto dto) { var result = await _leadService.IngresarActividadAsync(id, dto); return Ok(new { mensaje = result }); } } ``` #### 1.6 Autenticación JWT (reemplazo de FormsAuthentication) ```csharp // AuthController.cs [HttpPost("login")] public async Task Login(LoginRequest request) { var usuario = await _usuarioService.BuscarAsync(request.Rut, request.Clave); if (usuario == null) return Unauthorized(); var token = _jwtService.GenerateToken(usuario); var cookieOptions = new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.Strict, Expires = DateTime.UtcNow.AddMinutes(30) }; Response.Cookies.Append("SAM_TOKEN", token, cookieOptions); return Ok(new { nombre = usuario.Nombre, sede = usuario.Sede, token // también devuelto para apps mobile }); } ``` **Migración de cookies originales → JWT claims:** | Cookie original (FormsAuth) | Claims JWT | |---------------------------|------------| | `SAM_ID_SEDE` | `claim: sede` | | `SAM_USUARIO_NOMBRE` | `claim: name` | | `SAM_USUARIO_ID` | `claim: sub` (rut) | | `SAM_USUARIO_ID_PASS` | No se migra (inseguro) | #### 1.7 Reportes QuestPDF (reemplazo Crystal Reports) Cada uno de los 17 reportes Crystal → una clase QuestPDF. ```csharp // ReportService.cs public class ReportService : IReportService { public async Task GenerarContratoPdfAsync(int contratoId) { var data = await _contratoRepository.BuscarInfoPdfAsync(contratoId); // ... mapear a modelo del reporte return Document.Create(container => { container.Page(page => { page.Size(PageSizes.A4); page.Margin(2, Unit.Cm); page.Header().Element(c => ComposeHeader(c, data)); page.Content().Element(c => ComposeContent(c, data)); page.Footer().Element(ComposeFooter); }); }).GeneratePdf(); } private void ComposeHeader(IContainer container, ContratoPdfData data) { container.Row(r => { r.ConstantItem(100).Image("logo.png"); r.RelativeItem().AlignRight().Text($"Contrato N° {data.Numero}") .FontSize(16).Bold(); }); } } ``` **Endpoint de reportes:** ```csharp [HttpGet("reportes/contrato/{id}")] public async Task GetContratoPdf(int id) { var pdf = await _reportService.GenerarContratoPdfAsync(id); return File(pdf, "application/pdf", $"contrato_{id}.pdf"); } ``` --- ### FASE 2: SERVICES EXTERNOS — API APARTE (2-3 semanas) Proyecto separado `services-externos/` que expone servicios reutilizables. #### 2.1 LibreDTE (DTE Chile) Migrar `Web/FacturadorDTE/LibreDteConector.cs` → `ServicesExternos.API`. ```csharp [ApiController] [Route("api/dte")] public class DteController : ControllerBase { [HttpPost("emitir")] public async Task Emitir(DteEmissionRequest request) { // Lógica migrada de LibreDteConector.cs } [HttpGet("estado/{codigo}")] public async Task ConsultarEstado(string codigo) { // Consultar estado DTE } } ``` #### 2.2 Transbank Webpay Plus Migrar `Negocio/PagadorTransbank.cs` + `Datos/TransbankInfo.cs`. ```csharp [ApiController] [Route("api/pagos/transbank")] public class TransbankController : ControllerBase { [HttpPost("crear-transaccion")] public async Task CrearTransaccion(TransbankRequest request) { // Lógica de creación de transacción Webpay } [HttpPost("confirmar")] public async Task Confirmar(string token_ws) { // Confirmar y guardar voucher } } ``` #### 2.3 SMTP Email (con MailKit) Migrar `Negocio/Mails.cs` usando `MailKit` (reemplaza `SmtpClient` obsoleto). ```csharp [ApiController] [Route("api/email")] public class EmailController : ControllerBase { [HttpPost("send")] public async Task Send(EmailRequest request) { using var client = new SmtpClient(); await client.ConnectAsync(_smtpHost, _smtpPort, SecureSocketOptions.StartTls); await client.AuthenticateAsync(_smtpUser, _smtpPassword); // Enviar con templates HTML existentes } } ``` --- ### FASE 3: FRONTEND NEXT.JS (6-8 semanas) #### 3.1 Inicialización ```bash npx create-next-app@latest frontend --typescript --tailwind --app ``` #### 3.2 Layout Principal (SAM.Master) Replicar exactamente el `SAM.Master` actual en `app/layout.tsx`: - Sidebar con `boxicons` + FontAwesome - Logo SAM + favicon - Header con nombre de usuario + botón cerrar sesión - Timeout de inactividad de 30 min (misma lógica JS) - Responsive design (media queries originales) - Bootstrap 5 via CDN (misma versión) - Google Fonts Fira Sans ```tsx // app/layout.tsx export default function RootLayout({ children }: { children: React.ReactNode }) { return (
{/* Sidebar idéntica al original */}
SAM
  • Dashboard
  • {/* ... resto de links idénticos */}
{/* Nombre usuario + cerrar sesión */} {children}
); } ``` #### 3.3 Login Page ```tsx // app/login/page.tsx "use client"; export default function LoginPage() { const [rut, setRut] = useState(""); const [clave, setClave] = useState(""); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const res = await api.post("/auth/login", { rut, clave }); // Guardar token, redirigir a dashboard }; return (

Iniciar Sesión

setRut(e.target.value)} />
setClave(e.target.value)} />
); } ``` #### 3.4 Migración de Funcionalidad JS | Archivo JS original | Migración a TypeScript | |-------------------|----------------------| | `js/validarRUT.js` | `lib/validarRut.ts` | | `js/numeroTexto.js` | `lib/utils.ts` | | `js/toastr.min.css` | `react-hot-toast` package | | Inline JS (inactividad) | `hooks/useInactividad.ts` | | Inline JS (sidebar toggle) | `components/Sidebar.tsx` | #### 3.5 Manejo de Estado y DataTables | WebForms Control | Equivalente Next.js | |-----------------|-------------------| | `GridView` / `DataList` | `react-data-table-component` o `@tanstack/react-table` | | `Repeater` | `.map()` con fragmentos | | `UpdatePanel` (postback) | `useState` + fetch API | | `ViewState` | React state + React Query cache | | `LinkButton` | `button` + `onClick` + fetch | | `RequiredFieldValidator` | React Hook Form + Zod | | `Master.FindControl` | React Context + props | --- ### FASE 4: CONTENEDORES (1 semana) #### 4.1 Dockerfile Backend ```dockerfile FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY . . RUN dotnet restore RUN dotnet publish -c Release -o /app FROM mcr.microsoft.com/dotnet/aspnet:10.0 WORKDIR /app COPY --from=build /app . EXPOSE 8080 ENTRYPOINT ["dotnet", "Ventas.API.dll"] ``` #### 4.2 Dockerfile Frontend ```dockerfile FROM node:20-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-alpine AS runtime WORKDIR /app COPY --from=build /app/.next/standalone ./ COPY --from=build /app/public ./public EXPOSE 3000 ENV NODE_ENV=production CMD ["node", "server.js"] ``` #### 4.3 docker-compose.yml ```yaml version: '3.8' services: backend-api: build: context: ./backend dockerfile: src/Ventas.API/Dockerfile environment: - ConnectionStrings__Default=Host=192.168.0.254;Port=5432;Database=ichn;Username=postgres;Password=apoca11 - Jwt__Secret=${JWT_SECRET} - Jwt__Expiration=30 - LibreDte__UserHash=${LIBREDTE_USER_HASH} - LibreDte__Ambiente=${LIBREDTE_AMBIENTE} ports: - "5000:8080" networks: - ventas-network extra_hosts: - "host.docker.internal:192.168.0.254" services-externos: build: context: ./services-externos dockerfile: src/ServicesExternos.API/Dockerfile environment: - LibreDte__UserHash=${LIBREDTE_USER_HASH} - LibreDte__Ambiente=${LIBREDTE_AMBIENTE} - Transbank__ApiKey=${TRANSBANK_API_KEY} - Transbank__CommerceCode=${TRANSBANK_COMMERCE_CODE} - Smtp__Host=smtp.gmail.com - Smtp__Port=587 - Smtp__User=noresponder@norteamericano.cl - Smtp__Password=${SMTP_PASSWORD} - Google__ClientId=${GOOGLE_CLIENT_ID} - Google__ClientSecret=${GOOGLE_CLIENT_SECRET} ports: - "5001:8080" networks: - ventas-network frontend: build: ./frontend environment: - NEXT_PUBLIC_API_URL=http://backend-api:8080/api - NEXT_PUBLIC_SERVICES_URL=http://services-externos:8080/api ports: - "3000:3000" depends_on: - backend-api networks: - ventas-network networks: ventas-network: driver: bridge ``` #### 4.4 .env ```env # JWT JWT_SECRET=generar-clave-segura-aqui JWT_EXPIRATION=30 # LibreDTE LIBREDTE_USER_HASH=ZDLimhVCDEXoHR6yDTJpb80ta7KG4DqI LIBREDTE_AMBIENTE=0 # Transbank TRANSBANK_API_KEY=tu-api-key TRANSBANK_COMMERCE_CODE=tu-codigo-comercio # SMTP SMTP_PASSWORD=smith2251! # Google OAuth GOOGLE_CLIENT_ID=tu-client-id GOOGLE_CLIENT_SECRET=tu-client-secret ``` --- ### FASE 5: PRUEBAS INTEGRALES E2E (2-3 semanas) #### 5.1 Backend Unit Tests (xUnit + Moq) ```csharp public class LeadServiceTests { [Fact] public async Task Ingresar_WithValidData_ReturnsOk() { // Arrange var mockRepo = new Mock(); mockRepo.Setup(r => r.IngresarAsync(It.IsAny())) .ReturnsAsync("ok"); var service = new LeadService(mockRepo.Object); // Act var result = await service.IngresarAsync(validDto); // Assert Assert.Equal("ok", result); } } ``` #### 5.2 Backend Integration Tests (TestContainers + PostgreSQL) ```csharp public class LeadRepositoryTests : IClassFixture { [Fact] public async Task BuscarXestado_ReturnsLeads() { using var connection = new NpgsqlConnection(_fixture.ConnectionString); var repo = new LeadRepository(_fixture.ConnectionString); var result = await repo.BuscarXestadoAsync(ejecutivo: 1, estado: 1); Assert.NotEmpty(result); } } ``` #### 5.3 Frontend E2E (Playwright) **Escenarios a testear (mínimo 20):** | # | Escenario | Flujo | |---|-----------|-------| | 1 | Login exitoso | Login.aspx → Index.aspx | | 2 | Login fallido | Mostrar error | | 3 | Dashboard carga | Ver cards de resumen | | 4 | Listar leads | LeadV2.aspx carga DataTable | | 5 | Crear lead | Formulario submit → éxito | | 6 | Buscar lead por filtro | Búsqueda por estado/días | | 7 | Agregar actividad a lead | Modal actividad → submit | | 8 | Crear cotización | Lead → cotización → cálculo montos | | 9 | Pagar cotización | pago.aspx → Transbank | | 10 | Ver arqueo | Arqueo.aspx con datos | | 11 | Generar PDF contrato | Descarga PDF | | 12 | Generar PDF cotización | Descarga PDF | | 13 | Autorizar descuento | Flujo autorizador | | 14 | CRUD empresa | Crear/editar empresa | | 15 | Reporte ventas | ResumenVentas.aspx | | 16 | Cierre de sesión | Logout → Login | | 17 | Timeout inactividad | 30 min → redirect | | 18 | Reimpresión documento | Reimpresion.aspx | | 19 | Sidebar navegación | Todas las rutas | | 20 | Responsive mobile | Sidebar contraída | **Ejemplo test Playwright:** ```typescript // tests/e2e/login.spec.ts import { test, expect } from '@playwright/test'; test('login exitoso redirige a dashboard', async ({ page }) => { await page.goto('/login'); await page.fill('[name="rut"]', '12345678-5'); await page.fill('[name="clave"]', 'password'); await page.click('button:has-text("Ingresar")'); await expect(page).toHaveURL(/\/dashboard/); await expect(page.locator('.badge')).toContainText('Menu de Aplicaciones'); }); test('timeout de inactividad redirige a login', async ({ page }) => { // Mockear setTimeout para que dispare inmediatamente await page.clock.install(); await page.goto('/dashboard'); await page.clock.fastForward(1800001); await expect(page).toHaveURL(/\/login/); }); ``` #### 5.4 Visual Regression Testing ```typescript test('dashboard visual match', async ({ page }) => { await page.goto('/dashboard'); await expect(page).toHaveScreenshot('dashboard.png', { maxDiffPixelRatio: 0.02 }); }); ``` --- ## 5. CRONOGRAMA ESTIMADO | Fase | Actividad | Duración | Dependencias | |------|-----------|----------|-------------| | **Fase 1** | Backend .NET 10 — API REST | 6-8 semanas | DB ya operativa | | 1.1 | Crear solución + proyectos base | 2 días | - | | 1.2 | Ventas.Core (entidades, DTOs) | 1 semana | 1.1 | | 1.3 | Ventas.Infrastructure (EF + Dapper) | 2-3 semanas | 1.2 | | 1.4 | Ventas.Services (lógica negocio) | 2 semanas | 1.3 | | 1.5 | Ventas.API (controladores) | 1 semana | 1.4 | | 1.6 | Auth JWT | 3 días | 1.4 | | 1.7 | Reportes QuestPDF (17 reportes) | 2-3 semanas | 1.3 | | **Fase 2** | Services Externos API | 2-3 semanas | - | | 2.1 | LibreDTE | 1 semana | - | | 2.2 | Transbank | 1 semana | - | | 2.3 | Email (MailKit) | 2 días | - | | **Fase 3** | Frontend Next.js | 6-8 semanas | Fase 1 (API) | | 3.1 | Setup + Layout (SAM.Master replica) | 1 semana | - | | 3.2 | Login + Auth flow | 3 días | 3.1 | | 3.3 | Dashboard | 3 días | 3.2 | | 3.4 | Módulo Leads (5 páginas) | 1 semana | 3.2 | | 3.5 | Módulo Cotizaciones/Pagos | 1 semana | 3.2 | | 3.6 | Módulo Empresas | 1 semana | 3.2 | | 3.7 | Resto de páginas (~25) | 2-3 semanas | 3.2 | | **Fase 4** | Contenedores | 1 semana | Fases 1, 2, 3 | | 4.1 | Dockerfiles | 2 días | - | | 4.2 | docker-compose | 1 día | 4.1 | | 4.3 | Secretos y .env | 1 día | 4.2 | | **Fase 5** | Pruebas E2E | 2-3 semanas | Fases 1, 3 | | 5.1 | Unit tests backend | 1 semana | Fase 1 | | 5.2 | Integration tests | 1 semana | Fase 1 | | 5.3 | Playwright E2E (20 escenarios) | 1-2 semanas | Fase 3 | | **Total** | | **17-23 semanas** | | --- ## 6. RIESGOS Y MITIGACIONES | Riesgo | Impacto | Probabilidad | Mitigación | |--------|---------|-------------|------------| | SPs complejos con lógica MySQL no tienen equivalente directo en PostgreSQL | Alto | Media | Usar Dapper con consultas raw; dividir SPs grandes en funciones más pequeñas | | QuestPDF no puede replicar exactamente el layout visual de Crystal Reports | Alto | Alta | Revisar cada reporte visualmente; usar imágenes de fondo PNG si es necesario; considerar Puppeteer+HTML como fallback | | FormsAuthentication → JWT cambia toda la lógica de sesión | Medio | Media | Implementar middleware progresivo; mantener compatibilidad de cookies | | WebForms ViewState/postback → React state management | Medio | Media | Usar React Query para caché de datos del servidor; estado local para UI | | Los 40 code-behind mezclan lógica de UI con lógica de negocio | Alto | Alta | Refactorizar: UI → frontend, lógica → servicios backend, datos → repositorios | | Transbank SDK para .NET Framework puede no tener versión .NET 10 | Medio | Baja | Probar SDK .NET Standard 2.0; si no funciona, llamar API REST de Transbank directamente | | Estilos CSS originales pueden no renderizar exactamente igual en Next.js | Bajo | Media | Usar los mismos archivos CSS sin modificaciones; probar visualmente cada página | --- ## 7. EQUIPO RECOMENDADO | Rol | Cantidad | Dedicación | |-----|----------|------------| | Backend .NET 10 | 1-2 devs | Tiempo completo | | Frontend Next.js | 1-2 devs | Tiempo completo | | Base de datos PostgreSQL | 1 DBA | Partial | | QA / E2E Tests | 1 QA | Partial | --- ## 8. ENTREGABLES POR FASE | Fase | Entregable | |------|-----------| | **Fase 1** | Backend API funcionando con todos los endpoints, JWT auth, reportes QuestPDF | | **Fase 2** | Services Externos API con LibreDTE, Transbank, Email funcionando | | **Fase 3** | Frontend Next.js con UI idéntica, todas las páginas funcionales conectadas a la API | | **Fase 4** | docker-compose.yml con backend + frontend + services-externos, secretos en .env | | **Fase 5** | Suite de tests: unit tests (back), integration tests (back), E2E Playwright (20 escenarios) | --- ## 9. CONFIGURACIÓN DE CONEXIÓN A BASE DE DATOS ```json // appsettings.json (solo estructura, valores reales en env vars) { "ConnectionStrings": { "Default": "Host=192.168.0.254;Port=5432;Database=ichn;Username=postgres;Password=apoca11;Pooling=true;Maximum Pool Size=100;" }, "Jwt": { "Secret": "", "ExpirationMinutes": 30 }, "LibreDte": { "UserHash": "", "Ambiente": "0" }, "Smtp": { "Host": "smtp.gmail.com", "Port": 587, "User": "noresponder@norteamericano.cl", "Password": "" } } ``` ```csharp // Program.cs builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); builder.Services.AddScoped(sp => new LeadRepository(builder.Configuration.GetConnectionString("Default")!)); ```