Files
rm-ventas/plan_modulo_ventas.md
T
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

1223 lines
45 KiB
Markdown

# 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<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):**
```csharp
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:**
```xml
<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.
```csharp
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:**
```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<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)
```csharp
// 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.
```csharp
// 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:**
```csharp
[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`.
```csharp
[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`.
```csharp
[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).
```csharp
[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
```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 (
<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
```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 (
<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
```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<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)
```csharp
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:**
```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<VentasDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<ILeadRepository>(sp =>
new LeadRepository(builder.Configuration.GetConnectionString("Default")!));
```