Files
rm-ventas/plan_modulo_ventas.md
Nurfog 3d5419403d feat: core entities, infrastructure, and lead repositories
- 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
2026-07-07 17:45:49 -04:00

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 -
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)

// 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.csServicesExternos.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")!));