- 25 domain entities mapped from original Datos/ classes - 4 enums (EstadoLead, TipoPago, TipoDocumento, TipoDescuento) - DTOs for Lead, Auth flows - 6 repository interfaces - VentasDbContext with EF Core + Npgsql - LeadRepository (command SPs) and LeadQueryRepository (query SPs) with Dapper - Program.cs with JWT auth, CORS, DI setup - appsettings.json with connection string - .gitignore for bin/obj/node_modules - ROADMAP.md for daily progress tracking
45 KiB
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 | - |
| 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)
// 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)
// 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
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):
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):
public class VentasDbContext : DbContext
{
public VentasDbContext(DbContextOptions<VentasDbContext> options) : base(options) { }
// DbSets solo para entidades que usan FromSqlRaw
public DbSet<Lead> Leads { get; set; }
public DbSet<Contrato> Contratos { get; set; }
// ...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configurar entidades sin tabla (solo para SPs)
modelBuilder.Entity<Lead>(e => {
e.HasNoKey();
e.ToView(null);
});
}
}
Repositorio con Dapper (SPs complejos):
public class InformeRepository
{
private readonly string _connectionString;
public async Task<InformeVentasDto> 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<VentaResumen>(),
Detalle = multi.Read<VentaDetalle>().ToList(),
Totales = multi.ReadSingle<VentaTotales>()
};
}
}
Paquetes NuGet requeridos:
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.*" />
<PackageReference Include="Dapper" Version="2.*" />
<PackageReference Include="Npgsql" Version="10.*" />
<PackageReference Include="QuestPDF" Version="2025.*" />
<PackageReference Include="MailKit" Version="4.*" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.*" />
1.4 Ventas.Services — Lógica de Negocio
Migrar las 28 clases de Negocio/ a servicios con Inyección de Dependencias.
public class LeadService : ILeadService
{
private readonly ILeadRepository _leadRepository;
public LeadService(ILeadRepository leadRepository)
{
_leadRepository = leadRepository;
}
public async Task<Result<string>> 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:
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class LeadController : ControllerBase
{
private readonly ILeadService _leadService;
public LeadController(ILeadService leadService)
{
_leadService = leadService;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var result = await _leadService.BuscarIdAsync(id);
return Ok(result);
}
[HttpGet("buscar")]
public async Task<IActionResult> 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<IActionResult> Ingresar(LeadCreateDto dto)
{
var result = await _leadService.IngresarAsync(dto);
return Ok(new { mensaje = result });
}
[HttpPost("{id}/actividad")]
public async Task<IActionResult> 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)
// AuthController.cs
[HttpPost("login")]
public async Task<IActionResult> 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.
// ReportService.cs
public class ReportService : IReportService
{
public async Task<byte[]> 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:
[HttpGet("reportes/contrato/{id}")]
public async Task<IActionResult> 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.
[ApiController]
[Route("api/dte")]
public class DteController : ControllerBase
{
[HttpPost("emitir")]
public async Task<IActionResult> Emitir(DteEmissionRequest request)
{
// Lógica migrada de LibreDteConector.cs
}
[HttpGet("estado/{codigo}")]
public async Task<IActionResult> ConsultarEstado(string codigo)
{
// Consultar estado DTE
}
}
2.2 Transbank Webpay Plus
Migrar Negocio/PagadorTransbank.cs + Datos/TransbankInfo.cs.
[ApiController]
[Route("api/pagos/transbank")]
public class TransbankController : ControllerBase
{
[HttpPost("crear-transaccion")]
public async Task<IActionResult> CrearTransaccion(TransbankRequest request)
{
// Lógica de creación de transacción Webpay
}
[HttpPost("confirmar")]
public async Task<IActionResult> Confirmar(string token_ws)
{
// Confirmar y guardar voucher
}
}
2.3 SMTP Email (con MailKit)
Migrar Negocio/Mails.cs usando MailKit (reemplaza SmtpClient obsoleto).
[ApiController]
[Route("api/email")]
public class EmailController : ControllerBase
{
[HttpPost("send")]
public async Task<IActionResult> 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
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
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Fira+Sans&display=swap" rel="stylesheet" />
<link href="https://unpkg.com/boxicons@2.0.7/css/boxicons.min.css" rel="stylesheet" />
<link href="/css/style.css" rel="stylesheet" />
<link href="/css/ichn.css" rel="stylesheet" />
<link href="/css/ichnPaginas.css" rel="stylesheet" />
</head>
<body>
<AuthProvider>
<div className="sidebar">
{/* Sidebar idéntica al original */}
<div className="logo-details">
<img className="icon" src="/img/favicon.png" width="60" />
<div className="logo_name">SAM</div>
<i className='bx bx-menu' id="btn"></i>
</div>
<ul className="nav-list">
<li>
<Link href="/dashboard">
<i className="fas fa-home"></i>
<span className="links_name">Dashboard</span>
</Link>
</li>
{/* ... resto de links idénticos */}
</ul>
</div>
<section className="home-section alert-dark">
<Header /> {/* Nombre usuario + cerrar sesión */}
{children}
</section>
</AuthProvider>
</body>
</html>
);
}
3.3 Login Page
// 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 (
<div className="container mt-5">
<div className="row justify-content-center">
<div className="col-md-4">
<div className="card">
<div className="card-body">
<h3 className="text-center">Iniciar Sesión</h3>
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label>RUT</label>
<input className="form-control" value={rut}
onChange={e => setRut(e.target.value)} />
</div>
<div className="mb-3">
<label>Clave</label>
<input type="password" className="form-control" value={clave}
onChange={e => setClave(e.target.value)} />
</div>
<button type="submit" className="btn btn-primary w-100">Ingresar</button>
</form>
</div>
</div>
</div>
</div>
</div>
);
}
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
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
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
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
# 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)
public class LeadServiceTests
{
[Fact]
public async Task Ingresar_WithValidData_ReturnsOk()
{
// Arrange
var mockRepo = new Mock<ILeadRepository>();
mockRepo.Setup(r => r.IngresarAsync(It.IsAny<LeadCreateDto>()))
.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)
public class LeadRepositoryTests : IClassFixture<PostgreSqlFixture>
{
[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:
// 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
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
// 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": ""
}
}
// Program.cs
builder.Services.AddDbContext<VentasDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<ILeadRepository>(sp =>
new LeadRepository(builder.Configuration.GetConnectionString("Default")!));