From 3d5419403d03236452ff537e03406de816dba950 Mon Sep 17 00:00:00 2001 From: Nurfog Date: Tue, 7 Jul 2026 17:45:49 -0400 Subject: [PATCH] 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 --- .gitignore | 31 + ROADMAP.md | 122 ++ backend/Ventas.slnx | 8 + backend/src/Ventas.API/Program.cs | 60 + .../Ventas.API/Properties/launchSettings.json | 14 + backend/src/Ventas.API/Ventas.API.csproj | 19 + backend/src/Ventas.API/Ventas.API.http | 6 + .../Ventas.API/appsettings.Development.json | 8 + backend/src/Ventas.API/appsettings.json | 16 + backend/src/Ventas.Core/DTOs/LeadDto.cs | 59 + backend/src/Ventas.Core/DTOs/LoginRequest.cs | 7 + backend/src/Ventas.Core/DTOs/LoginResponse.cs | 9 + backend/src/Ventas.Core/Entities/Actividad.cs | 13 + backend/src/Ventas.Core/Entities/Alumno.cs | 21 + .../src/Ventas.Core/Entities/ArqueoCaja.cs | 10 + backend/src/Ventas.Core/Entities/Comuna.cs | 8 + backend/src/Ventas.Core/Entities/Contrato.cs | 15 + .../Ventas.Core/Entities/ContratoDetalle.cs | 14 + .../src/Ventas.Core/Entities/Cotizacion.cs | 25 + .../Ventas.Core/Entities/CotizacionDetalle.cs | 17 + backend/src/Ventas.Core/Entities/Curso.cs | 11 + backend/src/Ventas.Core/Entities/Descuento.cs | 12 + .../src/Ventas.Core/Entities/Diagnostico.cs | 12 + backend/src/Ventas.Core/Entities/Documento.cs | 15 + backend/src/Ventas.Core/Entities/Empresa.cs | 16 + backend/src/Ventas.Core/Entities/Horario.cs | 11 + backend/src/Ventas.Core/Entities/Jornada.cs | 8 + backend/src/Ventas.Core/Entities/Lead.cs | 20 + .../src/Ventas.Core/Entities/MotivoPerdido.cs | 8 + backend/src/Ventas.Core/Entities/Programa.cs | 9 + backend/src/Ventas.Core/Entities/Propuesta.cs | 14 + backend/src/Ventas.Core/Entities/Region.cs | 8 + backend/src/Ventas.Core/Entities/Sala.cs | 10 + backend/src/Ventas.Core/Entities/Sede.cs | 10 + backend/src/Ventas.Core/Entities/Tarifa.cs | 13 + .../src/Ventas.Core/Entities/TransbankInfo.cs | 21 + backend/src/Ventas.Core/Entities/Usuario.cs | 16 + backend/src/Ventas.Core/Enums/EstadoLead.cs | 12 + .../src/Ventas.Core/Enums/TipoDescuento.cs | 10 + .../src/Ventas.Core/Enums/TipoDocumento.cs | 9 + backend/src/Ventas.Core/Enums/TipoPago.cs | 11 + .../Interfaces/IAlumnoRepository.cs | 11 + .../Interfaces/IContratoRepository.cs | 7 + .../Interfaces/ICotizacionRepository.cs | 8 + .../Interfaces/ILeadQueryRepository.cs | 21 + .../Ventas.Core/Interfaces/ILeadRepository.cs | 20 + .../Interfaces/IUsuarioRepository.cs | 8 + backend/src/Ventas.Core/Ventas.Core.csproj | 9 + .../Data/VentasDbContext.cs | 20 + .../Repositories/LeadQueryRepository.cs | 154 +++ .../Repositories/LeadRepository.cs | 171 +++ .../Ventas.Infrastructure.csproj | 21 + .../Ventas.Services/Ventas.Services.csproj | 14 + plan_modulo_ventas.md | 1222 +++++++++++++++++ 54 files changed, 2424 insertions(+) create mode 100644 .gitignore create mode 100644 ROADMAP.md create mode 100644 backend/Ventas.slnx create mode 100644 backend/src/Ventas.API/Program.cs create mode 100644 backend/src/Ventas.API/Properties/launchSettings.json create mode 100644 backend/src/Ventas.API/Ventas.API.csproj create mode 100644 backend/src/Ventas.API/Ventas.API.http create mode 100644 backend/src/Ventas.API/appsettings.Development.json create mode 100644 backend/src/Ventas.API/appsettings.json create mode 100644 backend/src/Ventas.Core/DTOs/LeadDto.cs create mode 100644 backend/src/Ventas.Core/DTOs/LoginRequest.cs create mode 100644 backend/src/Ventas.Core/DTOs/LoginResponse.cs create mode 100644 backend/src/Ventas.Core/Entities/Actividad.cs create mode 100644 backend/src/Ventas.Core/Entities/Alumno.cs create mode 100644 backend/src/Ventas.Core/Entities/ArqueoCaja.cs create mode 100644 backend/src/Ventas.Core/Entities/Comuna.cs create mode 100644 backend/src/Ventas.Core/Entities/Contrato.cs create mode 100644 backend/src/Ventas.Core/Entities/ContratoDetalle.cs create mode 100644 backend/src/Ventas.Core/Entities/Cotizacion.cs create mode 100644 backend/src/Ventas.Core/Entities/CotizacionDetalle.cs create mode 100644 backend/src/Ventas.Core/Entities/Curso.cs create mode 100644 backend/src/Ventas.Core/Entities/Descuento.cs create mode 100644 backend/src/Ventas.Core/Entities/Diagnostico.cs create mode 100644 backend/src/Ventas.Core/Entities/Documento.cs create mode 100644 backend/src/Ventas.Core/Entities/Empresa.cs create mode 100644 backend/src/Ventas.Core/Entities/Horario.cs create mode 100644 backend/src/Ventas.Core/Entities/Jornada.cs create mode 100644 backend/src/Ventas.Core/Entities/Lead.cs create mode 100644 backend/src/Ventas.Core/Entities/MotivoPerdido.cs create mode 100644 backend/src/Ventas.Core/Entities/Programa.cs create mode 100644 backend/src/Ventas.Core/Entities/Propuesta.cs create mode 100644 backend/src/Ventas.Core/Entities/Region.cs create mode 100644 backend/src/Ventas.Core/Entities/Sala.cs create mode 100644 backend/src/Ventas.Core/Entities/Sede.cs create mode 100644 backend/src/Ventas.Core/Entities/Tarifa.cs create mode 100644 backend/src/Ventas.Core/Entities/TransbankInfo.cs create mode 100644 backend/src/Ventas.Core/Entities/Usuario.cs create mode 100644 backend/src/Ventas.Core/Enums/EstadoLead.cs create mode 100644 backend/src/Ventas.Core/Enums/TipoDescuento.cs create mode 100644 backend/src/Ventas.Core/Enums/TipoDocumento.cs create mode 100644 backend/src/Ventas.Core/Enums/TipoPago.cs create mode 100644 backend/src/Ventas.Core/Interfaces/IAlumnoRepository.cs create mode 100644 backend/src/Ventas.Core/Interfaces/IContratoRepository.cs create mode 100644 backend/src/Ventas.Core/Interfaces/ICotizacionRepository.cs create mode 100644 backend/src/Ventas.Core/Interfaces/ILeadQueryRepository.cs create mode 100644 backend/src/Ventas.Core/Interfaces/ILeadRepository.cs create mode 100644 backend/src/Ventas.Core/Interfaces/IUsuarioRepository.cs create mode 100644 backend/src/Ventas.Core/Ventas.Core.csproj create mode 100644 backend/src/Ventas.Infrastructure/Data/VentasDbContext.cs create mode 100644 backend/src/Ventas.Infrastructure/Repositories/LeadQueryRepository.cs create mode 100644 backend/src/Ventas.Infrastructure/Repositories/LeadRepository.cs create mode 100644 backend/src/Ventas.Infrastructure/Ventas.Infrastructure.csproj create mode 100644 backend/src/Ventas.Services/Ventas.Services.csproj create mode 100644 plan_modulo_ventas.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85a7bcc --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# .NET +bin/ +obj/ +*.user +*.suo +*.cache +*.dll +*.pdb +*.exe +*.runtimeconfig.json +*.deps.json +*.staticwebassets.endpoints.json +.vs/ + +# Node +node_modules/ +.next/ +*.tsbuildinfo + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp + +# Env +.env +*.env.local diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..ee83344 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,122 @@ +# Roadmap — Migración ModuloVentas + +> **Proyecto:** rm-ventas +> **Original:** `/home/juan/dev/ventas` (.NET Framework 4.7.2 / WebForms / MySQL) +> **Destino:** .NET 10 + PostgreSQL + Next.js +> **Inicio:** Julio 2026 + +--- + +## Estado Actual + +| Componente | Estado | +|-----------|--------| +| Backend .NET 10 — Skeleton (sln + 4 proyectos + NuGet) | ✅ CREADO | +| Entidades / DTOs / Interfaces (Ventas.Core) | ✅ CREADO | +| Infrastructure (EF Core + Dapper + Repos) | ✅ PARCIAL (DbContext + LeadRepository listos) | +| Services (lógica de negocio) | ❌ PENDIENTE | +| API Controllers | ❌ PENDIENTE | +| Auth JWT | ❌ PENDIENTE | +| Reportes QuestPDF (17 reportes) | ❌ PENDIENTE | +| ServicesExternos.API | ❌ PENDIENTE | +| Frontend Next.js | ❌ PENDIENTE | +| Contenedores (Docker) | ❌ PENDIENTE | +| Tests E2E | ❌ PENDIENTE | + +--- + +## Checklist por Fase + +### FASE 1: BACKEND .NET 10 — API REST (~6-8 semanas) + +- [x] **1.1** Crear solución + proyectos base +- [x] **1.2 Ventas.Core — Entidades y DTOs** (~1 sem) + - [x] Lead.cs + resto entidades (25 clases desde `Datos/`) + - [x] DTOs request/response (LeadDto, LoginRequest, etc.) + - [x] Enums (EstadoLead, TipoPago, TipoDocumento, TipoDescuento) + - [x] Interfaces repositorios (ILeadRepository, ILeadQueryRepository, IAlumnoRepository, IUsuarioRepository, IContratoRepository, ICotizacionRepository) +- [x] **1.3 Ventas.Infrastructure — Acceso a Datos** (~2-3 sem) + - [x] VentasDbContext.cs (EF Core + Npgsql) + - [x] LeadRepository.cs (SPs INSERT/UPDATE con Dapper) + - [x] LeadQueryRepository.cs (SPs SELECT con Dapper) + - [ ] Configurations/ (EF mapping) + - [ ] Resto repos (Alumno, Contrato, Cotizacion, etc.) + - [ ] SpComplexQueries.cs (Dapper multi-resultset) +- [ ] **1.4 Ventas.Services — Lógica de Negocio** (~2 sem) + - [ ] LeadService.cs + resto servicios (~15) +- [ ] **1.5 Ventas.API — Controladores REST** (~1 sem) + - [ ] AuthController.cs + - [ ] LeadController.cs + resto controllers (~20) + - [ ] Program.cs completo (DI, JWT, CORS) + - [ ] appsettings.json con connection strings +- [ ] **1.6 Auth JWT** (~3 días) + - [ ] JwtMiddleware.cs + - [ ] Login endpoint funcional + - [ ] Claims → cookies HttpOnly +- [ ] **1.7 Reportes QuestPDF** (~2-3 sem) + - [ ] ReportService.cs (base) + - [ ] 17 reportes (Contrato, Cotización, Arqueo, etc.) +- [ ] Dockerfile Backend + +### FASE 2: SERVICES EXTERNOS API (~2-3 semanas) + +- [ ] **2.0 Setup** + - [ ] ServicesExternos.sln + proyectos + - [ ] Program.cs + Dockerfile +- [ ] **2.1 LibreDTE** (~1 sem) +- [ ] **2.2 Transbank Webpay** (~1 sem) +- [ ] **2.3 Email con MailKit** (~2 días) + +### FASE 3: FRONTEND NEXT.JS (~6-8 semanas) + +- [ ] **3.1 Setup** (create-next-app, packages, config) +- [ ] **3.2 Layout Principal** (~1 sem) + - [ ] layout.tsx (sidebar + header = SAM.Master) + - [ ] Sidebar.tsx, Header.tsx + - [ ] CSS original (style.css, ichn.css, etc.) +- [ ] **3.3 Login + Auth** (~3 días) +- [ ] **3.4 Dashboard** (~3 días) +- [ ] **3.5 Módulo Leads** (~1 sem) — 3 páginas +- [ ] **3.6 Módulo Cotizaciones/Pagos** (~1 sem) +- [ ] **3.7 Módulo Empresas** (~1 sem) — 3 páginas +- [ ] **3.8 Resto páginas** (~2-3 sem) + - Arqueo, Cursos, Alumnos + - Reportes (3), Autorizador, Contacto +- [ ] Dockerfile Frontend + +### FASE 4: CONTENEDORES (~1 semana) + +- [ ] docker-compose.yml (backend-api + services-externos + frontend) +- [ ] .env.example +- [ ] .gitignore + +### FASE 5: PRUEBAS (~2-3 semanas) + +- [ ] Unit tests backend (xUnit + Moq) +- [ ] Integration tests (TestContainers + PostgreSQL) +- [ ] Playwright E2E (20 escenarios) +- [ ] Visual regression tests + +--- + +## Bitácora Diaria + +| Fecha | Fase | Qué se hizo | Siguiente paso | +|-------|------|-------------|----------------| +| 2026-07-07 | F1 | Entities (25), Enums (4), DTOs, Interfaces (6), DbContext, LeadRepository, LeadQueryRepository, DI setup, appsettings | Resto repos (Contrato, Cotizacion, Alumno) + Controllers | +| | | | | +| | | | | + +--- + +## Próximos Pasos Inmediatos + +1. ~~Completar `Ventas.Core/Entities/`~~ ✅ +2. ~~Configurar `VentasDbContext`~~ ✅ +3. ~~Crear `LeadRepository` + `LeadQueryRepository`~~ ✅ +4. **Agregar referencia Infrastructure a Ventas.API** ✅ +5. **Configurar JWT + CORS + DI en Program.cs** ✅ +6. **Actualizar `appsettings.json`** con connection string ✅ +7. **Seguir repositorios** por prioridad: Contrato → Cotizacion → Alumno → Usuario → Auth +8. **Crear Controllers** (empezar con AuthController + LeadController) +9. **Probar compilación** con `dotnet build` diff --git a/backend/Ventas.slnx b/backend/Ventas.slnx new file mode 100644 index 0000000..326c94a --- /dev/null +++ b/backend/Ventas.slnx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/backend/src/Ventas.API/Program.cs b/backend/src/Ventas.API/Program.cs new file mode 100644 index 0000000..d290b03 --- /dev/null +++ b/backend/src/Ventas.API/Program.cs @@ -0,0 +1,60 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Ventas.Infrastructure.Data; +using Ventas.Infrastructure.Repositories; +using Ventas.Core.Interfaces; + +var builder = WebApplication.CreateBuilder(args); + +var connectionString = builder.Configuration.GetConnectionString("Default")!; + +builder.Services.AddControllers(); +builder.Services.AddOpenApi(); + +builder.Services.AddDbContext(options => + options.UseNpgsql(connectionString)); + +builder.Services.AddScoped(sp => + new LeadRepository(connectionString)); +builder.Services.AddScoped(sp => + new LeadQueryRepository(connectionString)); + +var jwtSecret = builder.Configuration["Jwt:Secret"] ?? "default-dev-secret-change-in-production"; +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)) + }; + }); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod(); + }); +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseCors(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); diff --git a/backend/src/Ventas.API/Properties/launchSettings.json b/backend/src/Ventas.API/Properties/launchSettings.json new file mode 100644 index 0000000..20f400a --- /dev/null +++ b/backend/src/Ventas.API/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5087", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/backend/src/Ventas.API/Ventas.API.csproj b/backend/src/Ventas.API/Ventas.API.csproj new file mode 100644 index 0000000..b4b9809 --- /dev/null +++ b/backend/src/Ventas.API/Ventas.API.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/backend/src/Ventas.API/Ventas.API.http b/backend/src/Ventas.API/Ventas.API.http new file mode 100644 index 0000000..151a086 --- /dev/null +++ b/backend/src/Ventas.API/Ventas.API.http @@ -0,0 +1,6 @@ +@Ventas.API_HostAddress = http://localhost:5087 + +GET {{Ventas.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/backend/src/Ventas.API/appsettings.Development.json b/backend/src/Ventas.API/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/backend/src/Ventas.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/backend/src/Ventas.API/appsettings.json b/backend/src/Ventas.API/appsettings.json new file mode 100644 index 0000000..bad5a74 --- /dev/null +++ b/backend/src/Ventas.API/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "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 + } +} diff --git a/backend/src/Ventas.Core/DTOs/LeadDto.cs b/backend/src/Ventas.Core/DTOs/LeadDto.cs new file mode 100644 index 0000000..42f92e3 --- /dev/null +++ b/backend/src/Ventas.Core/DTOs/LeadDto.cs @@ -0,0 +1,59 @@ +namespace Ventas.Core.DTOs; + +public class LeadCreateDto +{ + 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 class LeadResponseDto +{ + 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 string? Ejecutivo { get; set; } + public string? Estado { get; set; } + public DateTime? FechaCreacion { get; set; } + public string? RutContacto { get; set; } +} + +public class LeadBuscarRequest +{ + public int? Ejecutivo { get; set; } + public int? Estado { get; set; } + public int? Dias { get; set; } + public string? TipoFiltro { get; set; } + public string? ValorBusqueda { get; set; } +} + +public class ContactoUpdateDto +{ + public string? Nombre { get; set; } + public string? Mail { get; set; } + public string? Telefono { get; set; } + public string? Rut { get; set; } +} + +public class ActividadCreateDto +{ + public string Tipo { get; set; } = string.Empty; + public string Descripcion { get; set; } = string.Empty; + public DateTime? FechaPlanificada { get; set; } +} + +public class PagoLeadDto +{ + public int LeadId { get; set; } + public int CotizacionId { get; set; } + public string FormaPago { get; set; } = string.Empty; + public int Monto { get; set; } + public string? CodigoAutorizacion { get; set; } + public string? DigitoTarjeta { get; set; } + public int Cuotas { get; set; } +} diff --git a/backend/src/Ventas.Core/DTOs/LoginRequest.cs b/backend/src/Ventas.Core/DTOs/LoginRequest.cs new file mode 100644 index 0000000..b3a923f --- /dev/null +++ b/backend/src/Ventas.Core/DTOs/LoginRequest.cs @@ -0,0 +1,7 @@ +namespace Ventas.Core.DTOs; + +public class LoginRequest +{ + public string Rut { get; set; } = string.Empty; + public string Clave { get; set; } = string.Empty; +} diff --git a/backend/src/Ventas.Core/DTOs/LoginResponse.cs b/backend/src/Ventas.Core/DTOs/LoginResponse.cs new file mode 100644 index 0000000..78d5a78 --- /dev/null +++ b/backend/src/Ventas.Core/DTOs/LoginResponse.cs @@ -0,0 +1,9 @@ +namespace Ventas.Core.DTOs; + +public class LoginResponse +{ + public string Token { get; set; } = string.Empty; + public string Nombre { get; set; } = string.Empty; + public string? Sede { get; set; } + public string? Perfil { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Actividad.cs b/backend/src/Ventas.Core/Entities/Actividad.cs new file mode 100644 index 0000000..9667f93 --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Actividad.cs @@ -0,0 +1,13 @@ +namespace Ventas.Core.Entities; + +public class Actividad +{ + public int Id { get; set; } + public int LeadId { get; set; } + public string? Tipo { get; set; } + public string? Descripcion { get; set; } + public string? UsuarioId { get; set; } + public DateTime? FechaCreacion { get; set; } + public DateTime? FechaPlanificada { get; set; } + public int? Estado { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Alumno.cs b/backend/src/Ventas.Core/Entities/Alumno.cs new file mode 100644 index 0000000..0b2dd26 --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Alumno.cs @@ -0,0 +1,21 @@ +namespace Ventas.Core.Entities; + +public class Alumno +{ + public string Rut { get; set; } = string.Empty; + public string Nombre { get; set; } = string.Empty; + public string? Paterno { get; set; } + public string? Materno { get; set; } + public string? Direccion { get; set; } + public string? Comuna { get; set; } + public int? ComunaId { get; set; } + public int? Nacionalidad { get; set; } + public string? FechaNacimiento { get; set; } + public string? Telefono { get; set; } + public string? Email { get; set; } + public string? Clave { get; set; } + public int? ColegioId { get; set; } + public int? ProfesionId { get; set; } + public int? OcupacionId { get; set; } + public string? ProfesionOficio { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/ArqueoCaja.cs b/backend/src/Ventas.Core/Entities/ArqueoCaja.cs new file mode 100644 index 0000000..b813356 --- /dev/null +++ b/backend/src/Ventas.Core/Entities/ArqueoCaja.cs @@ -0,0 +1,10 @@ +namespace Ventas.Core.Entities; + +public class ArqueoCaja +{ + public int Id { get; set; } + public string? UsuarioId { get; set; } + public DateTime? Fecha { get; set; } + public int? Monto { get; set; } + public string? Tipo { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Comuna.cs b/backend/src/Ventas.Core/Entities/Comuna.cs new file mode 100644 index 0000000..4aa897d --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Comuna.cs @@ -0,0 +1,8 @@ +namespace Ventas.Core.Entities; + +public class Comuna +{ + public int Id { get; set; } + public string Nombre { get; set; } = string.Empty; + public int? RegionId { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Contrato.cs b/backend/src/Ventas.Core/Entities/Contrato.cs new file mode 100644 index 0000000..3892031 --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Contrato.cs @@ -0,0 +1,15 @@ +namespace Ventas.Core.Entities; + +public class Contrato +{ + public int Id { get; set; } + public int? CotizacionId { get; set; } + public int? BoletaCKT { get; set; } + public DateTime? FechaContrato { get; set; } + public int? BoletaId { get; set; } + public int? VendedorId { get; set; } + public string? EmpresaId { get; set; } + public int? TipoVentaId { get; set; } + public int? FacturaId { get; set; } + public int? CantidadCursos { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/ContratoDetalle.cs b/backend/src/Ventas.Core/Entities/ContratoDetalle.cs new file mode 100644 index 0000000..c65567e --- /dev/null +++ b/backend/src/Ventas.Core/Entities/ContratoDetalle.cs @@ -0,0 +1,14 @@ +namespace Ventas.Core.Entities; + +public class ContratoDetalle +{ + public int Id { get; set; } + public int ContratoId { get; set; } + public string? EmpresaId { get; set; } + public string? AlumnoId { get; set; } + public string? CursoId { get; set; } + public DateTime? Fecha { get; set; } + public int? VendedorId { get; set; } + public int? TipoRegistro { get; set; } + public int? TipoAlumno { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Cotizacion.cs b/backend/src/Ventas.Core/Entities/Cotizacion.cs new file mode 100644 index 0000000..85346ed --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Cotizacion.cs @@ -0,0 +1,25 @@ +namespace Ventas.Core.Entities; + +public class Cotizacion +{ + public int Id { get; set; } + public int? LeadId { get; set; } + public string? Apoderado { get; set; } + public string? Vendedor { get; set; } + public int? SolicitudDescuentoId { get; set; } + public int? DescuentoId { get; set; } + public int? TipoDescuento { get; set; } + public string? Fecha { get; set; } + public int? Alumnos { get; set; } + public int? Curso { get; set; } + public int? Monto { get; set; } + public string? Validez { get; set; } + public string? EmpresaId { get; set; } + public int? MontoEmpresa { get; set; } + public int? MontoOtic { get; set; } + public int? MontoAlumno { get; set; } + public int? CotizacionTipo { get; set; } + public int? Estado { get; set; } + public string? Motivo { get; set; } + public string? OticId { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/CotizacionDetalle.cs b/backend/src/Ventas.Core/Entities/CotizacionDetalle.cs new file mode 100644 index 0000000..7eaeafa --- /dev/null +++ b/backend/src/Ventas.Core/Entities/CotizacionDetalle.cs @@ -0,0 +1,17 @@ +namespace Ventas.Core.Entities; + +public class CotizacionDetalle +{ + public int Id { get; set; } + public int CotizacionId { get; set; } + public string? AlumnoId { get; set; } + public string? ApoderadoId { get; set; } + public int? CursoId { get; set; } + public int? ProgramaId { get; set; } + public int? Cantidad { get; set; } + public int? TarifaId { get; set; } + public int? SedeId { get; set; } + public int? MontoAlumno { get; set; } + public int? MontoEmpresa { get; set; } + public int? MontoOtic { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Curso.cs b/backend/src/Ventas.Core/Entities/Curso.cs new file mode 100644 index 0000000..a87c42b --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Curso.cs @@ -0,0 +1,11 @@ +namespace Ventas.Core.Entities; + +public class Curso +{ + public int Id { get; set; } + public string Nombre { get; set; } = string.Empty; + public int? ProgramaId { get; set; } + public int? Producto { get; set; } + public int? Duracion { get; set; } + public bool? Activo { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Descuento.cs b/backend/src/Ventas.Core/Entities/Descuento.cs new file mode 100644 index 0000000..1782f62 --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Descuento.cs @@ -0,0 +1,12 @@ +namespace Ventas.Core.Entities; + +public class Descuento +{ + public int Id { get; set; } + public string Nombre { get; set; } = string.Empty; + public int? Monto { get; set; } + public DateTime? Inicio { get; set; } + public DateTime? Termino { get; set; } + public int? TipoDescuento { get; set; } + public bool? Activo { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Diagnostico.cs b/backend/src/Ventas.Core/Entities/Diagnostico.cs new file mode 100644 index 0000000..2c845e5 --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Diagnostico.cs @@ -0,0 +1,12 @@ +namespace Ventas.Core.Entities; + +public class Diagnostico +{ + public int Id { get; set; } + public string? AlumnoId { get; set; } + public int? CursoId { get; set; } + public int? LeadId { get; set; } + public string? UsuarioId { get; set; } + public DateTime? Fecha { get; set; } + public string? Resultado { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Documento.cs b/backend/src/Ventas.Core/Entities/Documento.cs new file mode 100644 index 0000000..85b4540 --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Documento.cs @@ -0,0 +1,15 @@ +namespace Ventas.Core.Entities; + +public class Documento +{ + public int Id { get; set; } + public string? NumeroInterno { get; set; } + public int? ContratoId { get; set; } + public string? EmpresaId { get; set; } + public int? TipoId { get; set; } + public int? ValorTotal { get; set; } + public int? CantidadCursos { get; set; } + public string? Glosa { get; set; } + public string? Ubicacion { get; set; } + public string? Usuario { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Empresa.cs b/backend/src/Ventas.Core/Entities/Empresa.cs new file mode 100644 index 0000000..32c73fb --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Empresa.cs @@ -0,0 +1,16 @@ +namespace Ventas.Core.Entities; + +public class Empresa +{ + public string Rut { get; set; } = string.Empty; + public string RazonSocial { get; set; } = string.Empty; + public string? Direccion { get; set; } + public string? ComunaId { get; set; } + public int? TipoId { get; set; } + public int? TamagnoId { get; set; } + public string? GiroNombre { get; set; } + public string? Contacto { get; set; } + public int? Fono { get; set; } + public string? Mail { get; set; } + public string? Origen { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Horario.cs b/backend/src/Ventas.Core/Entities/Horario.cs new file mode 100644 index 0000000..a9abb4b --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Horario.cs @@ -0,0 +1,11 @@ +namespace Ventas.Core.Entities; + +public class Horario +{ + public int Id { get; set; } + public int? BloqueId { get; set; } + public string? HoraInicio { get; set; } + public string? HoraTermino { get; set; } + public int? SedeId { get; set; } + public int? JornadaId { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Jornada.cs b/backend/src/Ventas.Core/Entities/Jornada.cs new file mode 100644 index 0000000..5c47f18 --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Jornada.cs @@ -0,0 +1,8 @@ +namespace Ventas.Core.Entities; + +public class Jornada +{ + public int Id { get; set; } + public string Nombre { get; set; } = string.Empty; + public bool? Activo { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Lead.cs b/backend/src/Ventas.Core/Entities/Lead.cs new file mode 100644 index 0000000..580e3ab --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Lead.cs @@ -0,0 +1,20 @@ +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; } + public string? RutContacto { get; set; } + public string? AlumnoId { get; set; } + public string? ClienteId { get; set; } + public int? Monto { get; set; } + public string? MensajeRecordatorio { get; set; } + public DateTime? FechaProximaGestion { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/MotivoPerdido.cs b/backend/src/Ventas.Core/Entities/MotivoPerdido.cs new file mode 100644 index 0000000..8914c94 --- /dev/null +++ b/backend/src/Ventas.Core/Entities/MotivoPerdido.cs @@ -0,0 +1,8 @@ +namespace Ventas.Core.Entities; + +public class MotivoPerdido +{ + public int Id { get; set; } + public string Nombre { get; set; } = string.Empty; + public bool? Activo { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Programa.cs b/backend/src/Ventas.Core/Entities/Programa.cs new file mode 100644 index 0000000..6143c9c --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Programa.cs @@ -0,0 +1,9 @@ +namespace Ventas.Core.Entities; + +public class Programa +{ + public int Id { get; set; } + public string Nombre { get; set; } = string.Empty; + public string? Tipo { get; set; } + public bool? Activo { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Propuesta.cs b/backend/src/Ventas.Core/Entities/Propuesta.cs new file mode 100644 index 0000000..c09f3bb --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Propuesta.cs @@ -0,0 +1,14 @@ +namespace Ventas.Core.Entities; + +public class Propuesta +{ + public int Id { get; set; } + public int? TipoPropuesta { get; set; } + public int? Estado { get; set; } + public int? TipoVenta { get; set; } + public string? Vendedor { get; set; } + public DateTime? Fecha { get; set; } + public int? Monto { get; set; } + public string? OticId { get; set; } + public string? EnvioLibre { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Region.cs b/backend/src/Ventas.Core/Entities/Region.cs new file mode 100644 index 0000000..b2ee2b5 --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Region.cs @@ -0,0 +1,8 @@ +namespace Ventas.Core.Entities; + +public class Region +{ + public int Id { get; set; } + public string Nombre { get; set; } = string.Empty; + public int? Numero { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Sala.cs b/backend/src/Ventas.Core/Entities/Sala.cs new file mode 100644 index 0000000..65437bc --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Sala.cs @@ -0,0 +1,10 @@ +namespace Ventas.Core.Entities; + +public class Sala +{ + public int Id { get; set; } + public string Nombre { get; set; } = string.Empty; + public int? SedeId { get; set; } + public int? Capacidad { get; set; } + public bool? Activo { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Sede.cs b/backend/src/Ventas.Core/Entities/Sede.cs new file mode 100644 index 0000000..a31d351 --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Sede.cs @@ -0,0 +1,10 @@ +namespace Ventas.Core.Entities; + +public class Sede +{ + public int Id { get; set; } + public string Nombre { get; set; } = string.Empty; + public string? Direccion { get; set; } + public int? ComunaId { get; set; } + public bool? Activo { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Tarifa.cs b/backend/src/Ventas.Core/Entities/Tarifa.cs new file mode 100644 index 0000000..896c5ca --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Tarifa.cs @@ -0,0 +1,13 @@ +namespace Ventas.Core.Entities; + +public class Tarifa +{ + public int Id { get; set; } + public int? Producto { get; set; } + public int? ProgramaId { get; set; } + public int? JornadaId { get; set; } + public int? SedeId { get; set; } + public int? Monto { get; set; } + public string? Fecha { get; set; } + public bool? Activo { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/TransbankInfo.cs b/backend/src/Ventas.Core/Entities/TransbankInfo.cs new file mode 100644 index 0000000..ca7d82f --- /dev/null +++ b/backend/src/Ventas.Core/Entities/TransbankInfo.cs @@ -0,0 +1,21 @@ +namespace Ventas.Core.Entities; + +public class TransbankInfo +{ + public int Id { get; set; } + public string? Token { get; set; } + public string? AccountingDate { get; set; } + public string? BuyOrder { get; set; } + public string? CardNumber { get; set; } + public string? AuthorizationCode { get; set; } + public string? PaymentTypeCode { get; set; } + public string? ResponseCode { get; set; } + public string? SharesNumber { get; set; } + public int? Amount { get; set; } + public string? CommerceCode { get; set; } + public string? SessionId { get; set; } + public DateTime? TransactionDate { get; set; } + public string? Vci { get; set; } + public string? Estado { get; set; } + public int? VendedorId { get; set; } +} diff --git a/backend/src/Ventas.Core/Entities/Usuario.cs b/backend/src/Ventas.Core/Entities/Usuario.cs new file mode 100644 index 0000000..8946711 --- /dev/null +++ b/backend/src/Ventas.Core/Entities/Usuario.cs @@ -0,0 +1,16 @@ +namespace Ventas.Core.Entities; + +public class Usuario +{ + public string Id { get; set; } = string.Empty; + public string Nombre { get; set; } = string.Empty; + public string? Paterno { get; set; } + public string? Materno { get; set; } + public string? Mail { get; set; } + public string? Fono1 { get; set; } + public string? Fono2 { get; set; } + public string? Clave { get; set; } + public int? SedeId { get; set; } + public string? Perfil { get; set; } + public bool? Activo { get; set; } +} diff --git a/backend/src/Ventas.Core/Enums/EstadoLead.cs b/backend/src/Ventas.Core/Enums/EstadoLead.cs new file mode 100644 index 0000000..1f97501 --- /dev/null +++ b/backend/src/Ventas.Core/Enums/EstadoLead.cs @@ -0,0 +1,12 @@ +namespace Ventas.Core.Enums; + +public enum EstadoLead +{ + Nuevo = 1, + Contactado = 2, + EnProceso = 3, + Cotizado = 4, + Ganado = 5, + Perdido = 6, + Archivado = 7 +} diff --git a/backend/src/Ventas.Core/Enums/TipoDescuento.cs b/backend/src/Ventas.Core/Enums/TipoDescuento.cs new file mode 100644 index 0000000..8eaeb01 --- /dev/null +++ b/backend/src/Ventas.Core/Enums/TipoDescuento.cs @@ -0,0 +1,10 @@ +namespace Ventas.Core.Enums; + +public enum TipoDescuento +{ + Porcentaje = 1, + MontoFijo = 2, + Promocion = 3, + Lider = 4, + Summer = 5 +} diff --git a/backend/src/Ventas.Core/Enums/TipoDocumento.cs b/backend/src/Ventas.Core/Enums/TipoDocumento.cs new file mode 100644 index 0000000..f14bd8c --- /dev/null +++ b/backend/src/Ventas.Core/Enums/TipoDocumento.cs @@ -0,0 +1,9 @@ +namespace Ventas.Core.Enums; + +public enum TipoDocumento +{ + Boleta = 1, + Factura = 2, + OrdenCompra = 3, + Sence = 4 +} diff --git a/backend/src/Ventas.Core/Enums/TipoPago.cs b/backend/src/Ventas.Core/Enums/TipoPago.cs new file mode 100644 index 0000000..a00566c --- /dev/null +++ b/backend/src/Ventas.Core/Enums/TipoPago.cs @@ -0,0 +1,11 @@ +namespace Ventas.Core.Enums; + +public enum TipoPago +{ + Efectivo = 1, + TarjetaCredito = 2, + TarjetaDebito = 3, + Transferencia = 4, + Webpay = 5, + Cheque = 6 +} diff --git a/backend/src/Ventas.Core/Interfaces/IAlumnoRepository.cs b/backend/src/Ventas.Core/Interfaces/IAlumnoRepository.cs new file mode 100644 index 0000000..0e6cfc2 --- /dev/null +++ b/backend/src/Ventas.Core/Interfaces/IAlumnoRepository.cs @@ -0,0 +1,11 @@ +namespace Ventas.Core.Interfaces; + +public interface IAlumnoRepository +{ + Task IngresarV2Async(string rut, string nombre, string paterno, string materno, string direccion, string comuna, string fecha, string fono, string mail, int ocupacion, string profeOficio); + Task IngresarV3Async(string rut, string nombre, string paterno, string materno, string direccion, string comuna, string fecha, string fono, string mail, int ocupacion, string profeOficio); + Task ActualizarAsync(string rut, string nombre, string paterno, string materno, string direccion, string comuna, int nacionalidad, string fecha, string fono, string mail, int ocupacion, string profesion); + Task IngresarApoderadoAsync(string rutApoderado, string rutAlumno, string nombre, string paterno, string materno, string direccion, string comuna, int nacionalidad, string fono, string mail); + Task ActualizarApoderadoAsync(string rutApoderado, string rutAlumno, string nombre, string paterno, string materno, string direccion, string comuna, int nacionalidad, string fono, string mail); + Task AsignarLeadDiagnosticoAsync(int diagnosticoId, int leadId); +} diff --git a/backend/src/Ventas.Core/Interfaces/IContratoRepository.cs b/backend/src/Ventas.Core/Interfaces/IContratoRepository.cs new file mode 100644 index 0000000..8e698db --- /dev/null +++ b/backend/src/Ventas.Core/Interfaces/IContratoRepository.cs @@ -0,0 +1,7 @@ +namespace Ventas.Core.Interfaces; + +public interface IContratoRepository +{ + Task IngresarAsync(int cotizacionId, int boletaCKT, string fechaContrato, int boletaId, int vendedorId); + Task IngresarDetalleAsync(int contratoId, string empresaId, string alumnoId, string cursoId, string fecha, int vendedor, int registroAcademico, int alumnoTipo); +} diff --git a/backend/src/Ventas.Core/Interfaces/ICotizacionRepository.cs b/backend/src/Ventas.Core/Interfaces/ICotizacionRepository.cs new file mode 100644 index 0000000..86a1398 --- /dev/null +++ b/backend/src/Ventas.Core/Interfaces/ICotizacionRepository.cs @@ -0,0 +1,8 @@ +namespace Ventas.Core.Interfaces; + +public interface ICotizacionRepository +{ + Task PersonaIngresarAsync(string apoderado, string vendedor, int solicitudDescuento, int descuento, int tipoDescuento, string fecha, int alumnos, int curso, int monto, string validez, int leadId); + Task PersonaPagarAsync(int cotizacionId); + Task DesactivarAsync(int cotizacionId); +} diff --git a/backend/src/Ventas.Core/Interfaces/ILeadQueryRepository.cs b/backend/src/Ventas.Core/Interfaces/ILeadQueryRepository.cs new file mode 100644 index 0000000..eeb257a --- /dev/null +++ b/backend/src/Ventas.Core/Interfaces/ILeadQueryRepository.cs @@ -0,0 +1,21 @@ +using Ventas.Core.Entities; + +namespace Ventas.Core.Interfaces; + +public interface ILeadQueryRepository +{ + Task>> BuscarAsync(int ejecutivo, int estado, int cantidadDias); + Task>> BuscarIDAsync(int idLead); + Task>> BuscarNuevosAsync(int ejecutivo); + Task>> BuscarGestionAsync(int ejecutivo, DateTime fecha); + Task>> BuscarMailAsync(string mail); + Task>> BuscarTituloAsync(string nombre); + Task>> BuscarXestadoAsync(int ejecutivo, int estado); + Task>> BuscarXfiltroAsync(string tipo, string busqueda); + Task>> BuscarXinformeAsync(string tipo); + Task>> MontosAsync(int ejecutivo); + Task>> ActividadesAsync(int leadId, string tipo); + Task>> MotivosPerdidoAsync(); + Task BuscarContactoAsync(int leadId); + Task>> BuscarResumenLeadEjeAsync(string ejecutivoId); +} diff --git a/backend/src/Ventas.Core/Interfaces/ILeadRepository.cs b/backend/src/Ventas.Core/Interfaces/ILeadRepository.cs new file mode 100644 index 0000000..69adba5 --- /dev/null +++ b/backend/src/Ventas.Core/Interfaces/ILeadRepository.cs @@ -0,0 +1,20 @@ +using Ventas.Core.DTOs; + +namespace Ventas.Core.Interfaces; + +public interface ILeadRepository +{ + Task IngresarAsync(LeadCreateDto dto); + Task IngresarV2Async(LeadCreateDto dto); + Task IngresarPagoAsync(PagoLeadDto dto); + Task ActualizarProductoAsync(int leadId, string producto); + Task EstadoUpdateAsync(int leadId, int estadoId); + Task IngresarLeadPerdidoAsync(int leadId, int motivo, int estado); + Task IngresarActividadAsync(int leadId, ActividadCreateDto dto); + Task IngresarActividadPlanAsync(int leadId, DateTime fecha, string actividad); + Task ActualizarActividadPlanAsync(int id, DateTime fecha, string actividad, int estado); + Task ActualizarContactoAsync(int leadId, ContactoUpdateDto dto); + Task IngresarAlumnoAsync(int leadId, string rut); + Task AgregarClienteAsync(int lead, string apoderadoId); + Task MensajeRecordarAsync(int leadId, string mensaje, DateTime fechaProxima); +} diff --git a/backend/src/Ventas.Core/Interfaces/IUsuarioRepository.cs b/backend/src/Ventas.Core/Interfaces/IUsuarioRepository.cs new file mode 100644 index 0000000..67c0b59 --- /dev/null +++ b/backend/src/Ventas.Core/Interfaces/IUsuarioRepository.cs @@ -0,0 +1,8 @@ +namespace Ventas.Core.Interfaces; + +public interface IUsuarioRepository +{ + Task BuscarAsync(string usuario, string clave); + Task InfoAsync(string usuario); + Task PerfilAsync(string usuario); +} diff --git a/backend/src/Ventas.Core/Ventas.Core.csproj b/backend/src/Ventas.Core/Ventas.Core.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/backend/src/Ventas.Core/Ventas.Core.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/backend/src/Ventas.Infrastructure/Data/VentasDbContext.cs b/backend/src/Ventas.Infrastructure/Data/VentasDbContext.cs new file mode 100644 index 0000000..5e5a7a9 --- /dev/null +++ b/backend/src/Ventas.Infrastructure/Data/VentasDbContext.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Ventas.Core.Entities; + +namespace Ventas.Infrastructure.Data; + +public class VentasDbContext : DbContext +{ + public VentasDbContext(DbContextOptions options) : base(options) { } + + public DbSet Leads { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(e => + { + e.HasNoKey(); + e.ToView(null); + }); + } +} diff --git a/backend/src/Ventas.Infrastructure/Repositories/LeadQueryRepository.cs b/backend/src/Ventas.Infrastructure/Repositories/LeadQueryRepository.cs new file mode 100644 index 0000000..bf26be3 --- /dev/null +++ b/backend/src/Ventas.Infrastructure/Repositories/LeadQueryRepository.cs @@ -0,0 +1,154 @@ +using Dapper; +using Npgsql; +using Ventas.Core.Interfaces; + +namespace Ventas.Infrastructure.Repositories; + +public class LeadQueryRepository : ILeadQueryRepository +{ + private readonly string _connectionString; + + public LeadQueryRepository(string connectionString) + { + _connectionString = connectionString; + } + + public async Task>> BuscarAsync(int ejecutivo, int estado, int cantidadDias) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "sige_sam_v3.BuscarLeadV3", + new { ejecutivo = ejecutivo, estadolead = estado, filtro = cantidadDias }, + commandType: System.Data.CommandType.StoredProcedure); + + return rows.Select(r => (Dictionary)(IDictionary)r!); + } + + public async Task>> BuscarIDAsync(int idLead) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "sige_sam_v3.BuscarLeadID", + new { id = idLead }, + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)r); + } + + public async Task>> BuscarNuevosAsync(int ejecutivo) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "sige_sam_v3.BuscarLeadNuevos", + new { ejecutivo = ejecutivo }, + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)r); + } + + public async Task>> BuscarGestionAsync(int ejecutivo, DateTime fecha) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "sige_sam_v3.BuscarLeadGestion", + new { ejecutivoid = ejecutivo, fecha = fecha }, + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)r); + } + + public async Task>> BuscarMailAsync(string mail) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "sige_sam_v3.BuscarLeadMail", + new { mailbuscar = mail }, + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)r); + } + + public async Task>> BuscarTituloAsync(string nombre) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "sige_sam_v3.BuscarLeadTitulo", + new { nombrebuscar = nombre }, + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)r); + } + + public async Task>> BuscarXestadoAsync(int ejecutivo, int estado) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "sige_sam_v3.Lead_buscarXestado", + new { userid = ejecutivo, estadoid = estado }, + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)r); + } + + public async Task>> BuscarXfiltroAsync(string tipo, string busqueda) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "sige_sam_v3.Lead_buscarXfiltro", + new { tipofiltro = tipo, valorbuscar = busqueda }, + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)r); + } + + public async Task>> BuscarXinformeAsync(string tipo) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "sige_sam_v3.Lead_InformeXhoy", + new { tipoinforme = tipo }, + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)r); + } + + public async Task>> MontosAsync(int ejecutivo) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "sige_sam_v3.BuscarLeadMontos", + new { ejecutivo = ejecutivo }, + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)r); + } + + public async Task>> ActividadesAsync(int leadId, string tipo) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "sige_sam_v3.BuscarActividadesLead", + new { id = leadId, tipoactividad = tipo }, + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)r); + } + + public async Task>> MotivosPerdidoAsync() + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "sige_sam_v3.BuscarMotivoLeadPerdido", + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)r); + } + + public async Task BuscarContactoAsync(int leadId) + { + using var connection = new NpgsqlConnection(_connectionString); + return await connection.QuerySingleOrDefaultAsync( + "sige_sam_v3.BuscarLeadContacto", + new { id = leadId }, + commandType: System.Data.CommandType.StoredProcedure) ?? string.Empty; + } + + public async Task>> BuscarResumenLeadEjeAsync(string ejecutivoId) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "sige_sam_v3.LeadCantidadEjecutivoNuevo", + new { userid = ejecutivoId }, + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)r); + } +} diff --git a/backend/src/Ventas.Infrastructure/Repositories/LeadRepository.cs b/backend/src/Ventas.Infrastructure/Repositories/LeadRepository.cs new file mode 100644 index 0000000..f18490e --- /dev/null +++ b/backend/src/Ventas.Infrastructure/Repositories/LeadRepository.cs @@ -0,0 +1,171 @@ +using Dapper; +using Npgsql; +using Ventas.Core.DTOs; +using Ventas.Core.Interfaces; + +namespace Ventas.Infrastructure.Repositories; + +public class LeadRepository : ILeadRepository +{ + private readonly string _connectionString; + + public LeadRepository(string connectionString) + { + _connectionString = connectionString; + } + + public async Task IngresarAsync(LeadCreateDto dto) + { + using var connection = new NpgsqlConnection(_connectionString); + using var reader = await connection.ExecuteReaderAsync( + "sige_sam_v3.GrabaLead", + new + { + nombrelead = dto.Nombre, + maillead = dto.Mail, + telefonolead = dto.Telefono, + productolead = dto.Producto, + contactolead = dto.Contacto, + ejecutivoid = dto.EjecutivoId + }, + commandType: System.Data.CommandType.StoredProcedure); + return "ok"; + } + + public async Task IngresarV2Async(LeadCreateDto dto) + { + using var connection = new NpgsqlConnection(_connectionString); + var result = await connection.QuerySingleOrDefaultAsync( + "sige_sam_v3.GrabaLead", + new + { + nombrelead = dto.Nombre, + maillead = dto.Mail, + telefonolead = dto.Telefono, + productolead = dto.Producto, + contactolead = dto.Contacto, + ejecutivoid = dto.EjecutivoId + }, + commandType: System.Data.CommandType.StoredProcedure); + return result ?? "ok"; + } + + public async Task IngresarPagoAsync(PagoLeadDto dto) + { + using var connection = new NpgsqlConnection(_connectionString); + await connection.ExecuteAsync( + "sige_sam_v3.GrabaPagoLead", + new + { + leadid = dto.LeadId, + cotiid = dto.CotizacionId, + formapago = dto.FormaPago, + valor = dto.Monto, + codauto = dto.CodigoAutorizacion, + digtar = dto.DigitoTarjeta, + cantcouta = dto.Cuotas + }, + commandType: System.Data.CommandType.StoredProcedure); + return "ok"; + } + + public async Task ActualizarProductoAsync(int leadId, string producto) + { + using var connection = new NpgsqlConnection(_connectionString); + await connection.ExecuteAsync( + "sam.ActualuzarLeadProducto", + new { leadid = leadId, productlead = producto }, + commandType: System.Data.CommandType.StoredProcedure); + return "ok"; + } + + public async Task EstadoUpdateAsync(int leadId, int estadoId) + { + using var connection = new NpgsqlConnection(_connectionString); + await connection.ExecuteAsync( + "sige_sam_v3.ActualizarEstadoLead", + new { leadid = leadId, estadolead = estadoId }, + commandType: System.Data.CommandType.StoredProcedure); + return "ok"; + } + + public async Task IngresarLeadPerdidoAsync(int leadId, int motivo, int estado) + { + using var connection = new NpgsqlConnection(_connectionString); + await connection.ExecuteAsync( + "sige_sam_v3.Lead_Ingresar_Perdido", + new { leadid = leadId, motivo = motivo, estado = estado }, + commandType: System.Data.CommandType.StoredProcedure); + return "ok"; + } + + public async Task IngresarActividadAsync(int leadId, ActividadCreateDto dto) + { + using var connection = new NpgsqlConnection(_connectionString); + await connection.ExecuteAsync( + "sige_sam_v3.GrabaActividadesLead", + new { leadid = leadId, tipo = dto.Tipo, descripcion = dto.Descripcion, usuarioid = "" }, + commandType: System.Data.CommandType.StoredProcedure); + return "ok"; + } + + public async Task IngresarActividadPlanAsync(int leadId, DateTime fecha, string actividad) + { + using var connection = new NpgsqlConnection(_connectionString); + await connection.ExecuteAsync( + "sige_sam_v3.IngresarActividadPlanLead", + new { leadid = leadId, fechaplan = fecha, descripcion = actividad }, + commandType: System.Data.CommandType.StoredProcedure); + return "ok"; + } + + public async Task ActualizarActividadPlanAsync(int id, DateTime fecha, string actividad, int estado) + { + using var connection = new NpgsqlConnection(_connectionString); + await connection.ExecuteAsync( + "sige_sam_v3.ActualizalLeadPlan", + new { fechaplan = fecha, gestion = actividad, estadoplan = estado, planid = id }, + commandType: System.Data.CommandType.StoredProcedure); + return "ok"; + } + + public async Task ActualizarContactoAsync(int leadId, ContactoUpdateDto dto) + { + using var connection = new NpgsqlConnection(_connectionString); + await connection.ExecuteAsync( + "sige_sam_v3.ActualizaLeadContacto", + new { leadid = leadId, nombrecontacto = dto.Nombre, mailcontacto = dto.Mail, fonocontacto = dto.Telefono, rutcontacto = dto.Rut }, + commandType: System.Data.CommandType.StoredProcedure); + return "ok"; + } + + public async Task IngresarAlumnoAsync(int leadId, string rut) + { + using var connection = new NpgsqlConnection(_connectionString); + await connection.ExecuteAsync( + "sige_sam_v3.ActualizarAlumnoLead", + new { leadid = leadId, alumnoid = rut }, + commandType: System.Data.CommandType.StoredProcedure); + return "ok"; + } + + public async Task AgregarClienteAsync(int lead, string apoderadoId) + { + using var connection = new NpgsqlConnection(_connectionString); + await connection.ExecuteAsync( + "sige_sam_v3.ActualizarLeadCliente", + new { leadid = lead, apoid = apoderadoId }, + commandType: System.Data.CommandType.StoredProcedure); + return "ok"; + } + + public async Task MensajeRecordarAsync(int leadId, string mensaje, DateTime fechaProxima) + { + using var connection = new NpgsqlConnection(_connectionString); + await connection.ExecuteAsync( + "sige_sam_v3.ActualizarRecordarLead", + new { leadid = leadId, mensajelead = mensaje, fechanextgestion = fechaProxima }, + commandType: System.Data.CommandType.StoredProcedure); + return "ok"; + } +} diff --git a/backend/src/Ventas.Infrastructure/Ventas.Infrastructure.csproj b/backend/src/Ventas.Infrastructure/Ventas.Infrastructure.csproj new file mode 100644 index 0000000..623ad31 --- /dev/null +++ b/backend/src/Ventas.Infrastructure/Ventas.Infrastructure.csproj @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/backend/src/Ventas.Services/Ventas.Services.csproj b/backend/src/Ventas.Services/Ventas.Services.csproj new file mode 100644 index 0000000..9fdf17a --- /dev/null +++ b/backend/src/Ventas.Services/Ventas.Services.csproj @@ -0,0 +1,14 @@ + + + + + + + + + net10.0 + enable + enable + + + diff --git a/plan_modulo_ventas.md b/plan_modulo_ventas.md new file mode 100644 index 0000000..2343ffd --- /dev/null +++ b/plan_modulo_ventas.md @@ -0,0 +1,1222 @@ +# Plan de Migración: ModuloVentas + +> **Repositorio:** https://git.norteamericano.cl/Nurfog/rm-ventas.git + +> **Prompt original:** +> necesito migrar este proyecto de .net framework 4.8 a .net core 10 (ya es lts), con conexiones a bases de datos desde mysql a postgresql, ademas que tenga un backend (api).net core 10 c# y un frontend en next.js +> los datos de conexion a la base de datos son los siguientes: +> host: 192.168.0.254 +> puerto: 5432 +> usuario: postgres +> contraseña: apoca11 +> database: ichn +> necesito que se cree un plan con los siguientes parametros: +> analizar completamente y minuciosamente el proyecto. +> los reportes de crystal reports tienen que ser migrados al formato de questpdf +> la estrategia de datos sera con entity frameworks para sp simples y ejecutandolos en la base de datos los sp complejos +> respecto a los facturadores, no usamos facturachile ni el soap wsAmazonSige +> todos los servicios externos los quiero en un proyecto aparte como backend como api, ya que varios proyectos mas lo usan +> ademas usar contenedores para backend y otro para frontend +> secretos en variables de entorno +> crear pruebas integrales e2e +> graficamente, el frontend tiene que se identico al original. +> todas las estructuras y datos ya se encuentan en la base de datos (nada se migra) + +--- + +## 1. ANÁLISIS DEL PROYECTO ACTUAL + +### 1.1 Estructura General + +``` +/home/juan/dev/ventas/ +├── ModuloVentas.sln # Solución Visual Studio 2022 +├── ModuloVentas.csproj # Consola bootstrap (obsoleto) +├── App.config +│ +├── Datos/ # Capa de Datos - Class Library +│ ├── Datos.csproj # .NET Framework 4.7.2 +│ ├── packages.config # MySql.Data 9.3, MySqlConnector 2.6 +│ ├── Conexion.cs # Conexión MySQL (hardcodeada) +│ └── 25 clases de entidad: +│ Alumno, ArqueoCaja, Comuna, Contrato, Cotizacion, +│ CotizacionInscripcion, Curso, Descuento, Diagnostico, +│ Documento, Ejecutivo, Empresa, Horario, Informes, +│ Jornada, Lead, Programa, Propuesta, Region, Sala, +│ SamAntiguo, Samold, Sede, Tarifa, TransbankInfo, Usuario +│ +├── Negocio/ # Capa de Negocio - Class Library +│ ├── Negocio.csproj # .NET Framework 4.7.2 +│ └── 28 clases: +│ Nalumno, NArqueoCaja, Ncomuna, Ncontrato, Ncotizacion, +│ NcotizacionInscripcion, Ncurso, Ndescuento, Ndiagnostico, +│ Ndocumento, Nejecutivo, Nempresa, Nhorario, Ninforme, +│ Njornada, Nlead, Nmail, Nprograma, Npropuesta, Nregion, +│ Nsala, NsamAntiguo, Nsamold, Nsede, Ntarifa, Nusuario, +│ PagadorTransbank, ValidarRut, IchnEncrypt, FechaApi, Mails +│ +└── Web/ # Presentación - ASP.NET WebForms + ├── Web.csproj # .NET Framework 4.7.2 + ├── Web.config # Config principal + ├── packages.config # 15 paquetes NuGet + ├── SAM.Master / SAM.Master.cs # Master Page (sidebar + layout) + ├── 40 páginas .aspx + code-behind + ├── 17 reportes Crystal Reports (.rpt) + ├── 12 Typed DataSets (.xsd) + ├── 2 servicios WCF (Connected Services) + ├── css/, js/, vendor/, img/ + └── Template/, correo/ (HTML emails) +``` + +### 1.2 Tecnologías Actuales + +| Componente | Tecnología | Versión | +|-----------|-----------|---------| +| **Framework** | .NET Framework | 4.7.2 | +| **Frontend** | ASP.NET WebForms + Bootstrap 5 + jQuery | - | +| **Base de datos** | MySQL | 9.3 (aws ec2) | +| **ORM** | Ninguno (ADO.NET puro) | - | +| **Reportes** | Crystal Reports | 13.0.4000 | +| **Pagos** | Transbank SDK | 6.0.0 | +| **DTE Chile** | LibreDTE (REST) + WSFacturaChile (SOAP) | - | +| **ERP/SIGE** | wsAmazonSige (SOAP) | - | +| **Auth** | FormsAuthentication + cookies | - | +| **Email** | SmtpClient (Gmail SMTP) | - | +| **Tests** | Ninguno | - | +| **Contenedores** | Ninguno | - | + +### 1.3 Bases de Datos Originales (MySQL) + +| Base de Datos | Uso | +|-------------|-----| +| `sige_sam_V3` | Principal - leads, cotizaciones, contratos, alumnos | +| `sige_sam` | Legado SAM | +| `caja_tbk` | Transbank/webpay | +| `sige_sam_empresa` | Módulo empresas | + +### 1.4 Patrón de Acceso a Datos (Actual) + +```csharp +// Patrón repetido en las 25 clases de Datos/ +private readonly Conexion Conexion = new Conexion(); +private MySqlCommand Comando; +private MySqlDataAdapter Adaptador; +private MySqlDataReader Leer; + +// Ejemplo típico: +Comando = new MySqlCommand("sige_sam_V3.GrabaLead", Conexion.AbrirConnectionMySql()) +{ + CommandType = CommandType.StoredProcedure +}; +Comando.Parameters.AddWithValue("@param", valor); +Adaptador = new MySqlDataAdapter(Comando); +Adaptador.Fill(tabla); +Conexion.CerrarConnectionMysql(); +``` + +### 1.5 Conexiones Actuales (hardcodeadas en Conexion.cs) + +```csharp +// 4 métodos de conexión, todas hardcodeadas: +AbrirConnectionMySql() → sige_sam_V3 @ AWS +AbrirConnectionAmazonSamOld()→ sige_sam @ AWS +AbrirConnectionTBK() → caja_tbk @ AWS +AbrirConnectionMySqlEmp() → sige_sam_empresa @ AWS +``` + +### 1.6 Crystal Reports - Inventario Completo + +| # | Archivo .rpt | Ubicación | Uso en Code-behind | +|---|-------------|-----------|-------------------| +| 1 | Arqueo.rpt | Plantillas/ | Arqueo.aspx.cs | +| 2 | CrystalReportAnexo.rpt | Plantillas/ + Planillas/ | FacturadorBoleta.aspx.cs, pago.aspx.cs | +| 3 | CrystalReportContrato.rpt | Plantillas/ + Planillas/ | FacturadorBoleta.aspx.cs, pago.aspx.cs | +| 4 | CrystalReportContratoBlack.rpt | Planillas/ | FacturadorBoleta.aspx.cs | +| 5 | CrystalReportCotizacion.rpt | Plantillas/ | pago.aspx.cs, Panel.aspx.cs | +| 6 | ReportPresupuesto.rpt | Plantillas/ | - | +| 7 | CAEMP.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | +| 8 | CAEMPSNC.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | +| 9 | CAEMPV2.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | +| 10 | CC_EMP.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | +| 11 | CC_EMPCSD.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | +| 12 | CC_PRS.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | +| 13 | CC_SNC.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | +| 14 | CotizacionCursoCerrado.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | +| 15 | CotizacionPlanCentral.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | +| 16 | PropuestaComercial.rpt | PlantillasEmpresas/ | AdminEmpresa.aspx.cs | +| 17 | ReportPresupuesto.rpt | Plantillas/ | - | + +### 1.7 Servicios Externos + +| Servicio | Tipo | Endpoint | Uso | +|---------|------|----------|-----| +| **wsAmazonSige** | SOAP WCF | `https://api.norteamericano.cl/wsSige/Service.asmx` | Sincronización ventas con ERP/SIGE | +| **WSFacturaChile** | SOAP WCF | `http://ws.facturachile.cl/WSServicios.asmx` | Emisión de boletas electrónicas | +| **LibreDTE** | REST | `https://libredte.cl` | Emisión DTE (facturación electrónica chilena) | +| **Transbank Webpay** | SDK | API Transbank | Pagos online con tarjetas | +| **Google OAuth** | OAuth 2.0 | Google APIs | Login con Google | +| **SMTP Email** | SMTP | `smtp.gmail.com:587` | Correos transaccionales | + +--- + +## 2. ARQUITECTURA OBJETIVO + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Frontend (Next.js) │ +│ Misma UI exacta: Bootstrap 5 + FontAwesome + CSS original │ +│ SAM.Master → Layout.tsx (sidebar idéntica + responsive) │ +│ 40 páginas .aspx → 40 rutas en App Router │ +│ DataList/GridView → DataTables (react-data-table-component) │ +│ Reportes → PDF vía API (QuestPDF) │ +│ Auth → JWT + cookies HttpOnly │ +└──────────────────────┬──────────────────────────────────────┘ + │ HTTP (fetch/axios) +┌──────────────────────▼──────────────────────────────────────┐ +│ Backend API (.NET 10) │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Ventas.API (Controllers REST) │ │ +│ │ /api/auth, /api/lead, /api/contrato, │ │ +│ │ /api/cotizacion, /api/alumno, /api/arqueo, │ │ +│ │ /api/curso, /api/descuento, /api/documento, │ │ +│ │ /api/ejecutivo, /api/empresa, /api/horario, │ │ +│ │ /api/informe, /api/jornada, /api/programa, │ │ +│ │ /api/propuesta, /api/region, /api/sala, │ │ +│ │ /api/sede, /api/tarifa, /api/usuario │ │ +│ └──────────────────────┬──────────────────────────────┘ │ +│ ┌──────────────────────▼──────────────────────────────┐ │ +│ │ Ventas.Services (Business Logic) │ │ +│ │ LeadService, ContratoService, CotizacionService, │ │ +│ │ AlumnoService, ArqueoService, UsuarioService, etc. │ │ +│ │ EmailService, ReportService (QuestPDF) │ │ +│ └──────────────────────┬──────────────────────────────┘ │ +│ ┌──────────────────────▼──────────────────────────────┐ │ +│ │ Ventas.Infrastructure (Data Layer) │ │ +│ │ │ EF Core → SPs simples (FromSql, ExecuteSqlRaw) │ │ +│ │ │ Dapper → SPs complejos (multi-resultset) │ │ +│ │ │ NpgsqlConnection → PostgreSQL │ │ +│ │ └── DbContext, Repositories, UnitOfWork │ │ +│ └──────────────────────┬──────────────────────────────┘ │ +│ ┌──────────────────────▼──────────────────────────────┐ │ +│ │ Ventas.Core (Domain) │ │ +│ │ Entidades, DTOs, Interfaces, Enums │ │ +│ └─────────────────────────────────────────────────────┘ │ +└──────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────────────┐ +│ PostgreSQL (192.168.0.254:5432) │ +│ Database: ichn │ +│ Tablas, SPs y datos ya migrados │ +└─────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────┐ +│ ServicesExternos.API (Proyecto separado) │ +│ ├── /api/dte (LibreDTE - emisión DTE Chile) │ +│ ├── /api/pagos/transbank (Transbank Webpay Plus) │ +│ ├── /api/email (SMTP con MailKit) │ +│ ├── /api/auth/google (Google OAuth) │ +│ │ │ +│ ⚡ Reutilizable por otros proyectos de la organización │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. ESTRUCTURA DE DIRECTORIOS (NUEVA) + +``` +/ventas/ +├── docker-compose.yml +├── .env.example +├── .gitignore +│ +├── backend/ +│ ├── Ventas.sln +│ ├── src/ +│ │ ├── Ventas.API/ +│ │ │ ├── Controllers/ +│ │ │ │ ├── AuthController.cs +│ │ │ │ ├── LeadController.cs +│ │ │ │ ├── ContratoController.cs +│ │ │ │ ├── CotizacionController.cs +│ │ │ │ ├── AlumnoController.cs +│ │ │ │ ├── ArqueoController.cs +│ │ │ │ ├── CursoController.cs +│ │ │ │ ├── DescuentoController.cs +│ │ │ │ ├── DocumentoController.cs +│ │ │ │ ├── EjecutivoController.cs +│ │ │ │ ├── EmpresaController.cs +│ │ │ │ ├── HorarioController.cs +│ │ │ │ ├── InformeController.cs +│ │ │ │ ├── JornadaController.cs +│ │ │ │ ├── ProgramaController.cs +│ │ │ │ ├── PropuestaController.cs +│ │ │ │ ├── RegionController.cs +│ │ │ │ ├── SalaController.cs +│ │ │ │ ├── SedeController.cs +│ │ │ │ ├── TarifaController.cs +│ │ │ │ └── UsuarioController.cs +│ │ │ ├── Middleware/ +│ │ │ │ └── JwtMiddleware.cs +│ │ │ ├── Program.cs +│ │ │ ├── appsettings.json +│ │ │ └── Dockerfile +│ │ ├── Ventas.Core/ +│ │ │ ├── Entities/ +│ │ │ │ ├── Lead.cs +│ │ │ │ ├── Contrato.cs +│ │ │ │ ├── Cotizacion.cs +│ │ │ │ ├── Alumno.cs +│ │ │ │ ├── ArqueoCaja.cs +│ │ │ │ ├── Curso.cs +│ │ │ │ ├── Descuento.cs +│ │ │ │ ├── Documento.cs +│ │ │ │ ├── Ejecutivo.cs +│ │ │ │ ├── Empresa.cs +│ │ │ │ ├── Horario.cs +│ │ │ │ ├── Jornada.cs +│ │ │ │ ├── Programa.cs +│ │ │ │ ├── Propuesta.cs +│ │ │ │ ├── Region.cs +│ │ │ │ ├── Sala.cs +│ │ │ │ ├── Sede.cs +│ │ │ │ ├── Tarifa.cs +│ │ │ │ ├── Usuario.cs +│ │ │ │ └── Comuna.cs +│ │ │ ├── DTOs/ +│ │ │ ├── Enums/ +│ │ │ └── Interfaces/ +│ │ │ ├── ILeadRepository.cs +│ │ │ ├── IContratoRepository.cs +│ │ │ ├── ICotizacionRepository.cs +│ │ │ └── ... +│ │ ├── Ventas.Infrastructure/ +│ │ │ ├── Data/ +│ │ │ │ ├── VentasDbContext.cs +│ │ │ │ └── Configurations/ +│ │ │ ├── Repositories/ +│ │ │ │ ├── LeadRepository.cs +│ │ │ │ ├── ContratoRepository.cs +│ │ │ │ └── ... +│ │ │ └── Dapper/ +│ │ │ └── SpComplexQueries.cs +│ │ └── Ventas.Services/ +│ │ ├── LeadService.cs +│ │ ├── ContratoService.cs +│ │ ├── CotizacionService.cs +│ │ ├── AlumnoService.cs +│ │ ├── ArqueoService.cs +│ │ ├── UsuarioService.cs +│ │ ├── MailsService.cs +│ │ ├── ReportService.cs (QuestPDF) +│ │ └── ... +│ └── tests/ +│ ├── Ventas.UnitTests/ +│ │ ├── Services/ +│ │ └── ... +│ └── Ventas.IntegrationTests/ +│ ├── Repositories/ +│ └── ... +│ +├── services-externos/ +│ ├── ServicesExternos.sln +│ ├── src/ +│ │ ├── ServicesExternos.API/ +│ │ │ ├── Controllers/ +│ │ │ │ ├── DteController.cs +│ │ │ │ ├── TransbankController.cs +│ │ │ │ ├── EmailController.cs +│ │ │ │ └── GoogleAuthController.cs +│ │ │ ├── Program.cs +│ │ │ └── Dockerfile +│ │ ├── ServicesExternos.Core/ +│ │ └── ServicesExternos.Infrastructure/ +│ └── tests/ +│ +├── frontend/ +│ ├── next.config.js +│ ├── package.json +│ ├── tsconfig.json +│ ├── Dockerfile +│ ├── public/ +│ │ ├── img/ +│ │ │ ├── favicon.png +│ │ │ ├── ichnHD.png +│ │ │ ├── Invertido_ICN.png +│ │ │ ├── samito.png +│ │ │ └── 03.jpg +│ │ └── ... +│ └── src/ +│ ├── app/ +│ │ ├── layout.tsx # = SAM.Master (sidebar + header) +│ │ ├── page.tsx # redirect a /dashboard +│ │ ├── login/ +│ │ │ └── page.tsx # = Login.aspx +│ │ ├── dashboard/ +│ │ │ └── page.tsx # = Index.aspx +│ │ ├── leads/ +│ │ │ ├── page.tsx # = LeadV2.aspx +│ │ │ ├── [id]/ +│ │ │ │ └── page.tsx # = LeadPanel.aspx +│ │ │ └── nuevo/ +│ │ │ └── page.tsx # = Lead.aspx +│ │ ├── empresas/ +│ │ │ ├── page.tsx # = Empresas.aspx +│ │ │ ├── [id]/ +│ │ │ │ └── page.tsx # = AdminEmpresa.aspx +│ │ │ └── ventas/ +│ │ │ └── page.tsx # = VentaEmpresa.aspx +│ │ ├── cotizaciones/ +│ │ │ └── page.tsx # = pago.aspx +│ │ ├── arqueo/ +│ │ │ └── page.tsx # = Arqueo.aspx +│ │ ├── cursos/ +│ │ │ └── page.tsx # = Aperturas.aspx +│ │ ├── alumnos/ +│ │ │ └── page.tsx # = Ficha.aspx +│ │ ├── reportes/ +│ │ │ ├── ventas/ +│ │ │ │ └── page.tsx # = ResumenVentas.aspx +│ │ │ ├── ventas-empresas/ +│ │ │ │ └── page.tsx # = ResumenVentasEmp.aspx +│ │ │ └── reimpresion/ +│ │ │ └── page.tsx # = Reimpresion.aspx +│ │ ├── autorizador/ +│ │ │ └── page.tsx # = Autorizador.aspx +│ │ ├── contacto/ +│ │ │ └── page.tsx # = Contacto.aspx +│ │ └── pdf/ # PDFs generados por API +│ ├── components/ +│ │ ├── Sidebar.tsx # Navegación idéntica al original +│ │ ├── Header.tsx # Barra superior con usuario +│ │ ├── DataTable.tsx # Reemplazo de DataList/GridView +│ │ ├── ModalConfirmacion.tsx +│ │ ├── LoadingSpinner.tsx +│ │ └── ... +│ ├── services/ +│ │ └── api.ts # Cliente HTTP centralizado +│ ├── hooks/ +│ │ ├── useAuth.ts +│ │ ├── useInactividad.ts # Timeout 30 min (original) +│ │ └── ... +│ ├── lib/ +│ │ ├── validarRut.ts # Migrado de validarRUT.js +│ │ └── utils.ts +│ └── styles/ +│ ├── globals.css +│ ├── style.css # Mismo CSS original +│ ├── ichn.css # Mismo CSS original +│ ├── ichnPaginas.css # Mismo CSS original +│ └── checkRadio.css # Mismo CSS original +│ +└── scripts/ + ├── migracion/ # Scripts de referencia + └── deploy/ +``` + +--- + +## 4. FASES DE MIGRACIÓN + +### FASE 1: BACKEND .NET 10 — API REST (6-8 semanas) + +#### 1.1 Crear solución y proyectos base + +```bash +dotnet new sln -n Ventas +dotnet new webapi -n Ventas.API +dotnet new classlib -n Ventas.Core +dotnet new classlib -n Ventas.Infrastructure +dotnet new classlib -n Ventas.Services +dotnet sln add src/**/*.csproj +``` + +#### 1.2 Ventas.Core — Entidades y DTOs + +Mapear todas las entidades desde las 25 clases de `Datos/`. Crear DTOs para request/response de cada endpoint. + +**Ejemplo Lead (Core):** +```csharp +namespace Ventas.Core.Entities; + +public class Lead +{ + public int Id { get; set; } + public string Nombre { get; set; } = string.Empty; + public string? Mail { get; set; } + public string? Telefono { get; set; } + public string? Producto { get; set; } + public int? Contacto { get; set; } + public int? EjecutivoId { get; set; } + public int? Estado { get; set; } + public DateTime? FechaCreacion { get; set; } + // ... resto de propiedades +} +``` + +#### 1.3 Ventas.Infrastructure — Acceso a Datos + +**Estrategia EF Core + Dapper:** + +| Tipo de SP | Estrategia | Ejemplo | +|-----------|-----------|---------| +| **SP simple** (1 tabla de vuelta) | `EF Core FromSqlRaw` | `Lead_buscarXestado` | +| **SP simple** (INSERT/UPDATE sin return) | `EF Core ExecuteSqlRaw` | `GrabaLead` | +| **SP complejo** (multi-resultset) | **Dapper** `QueryMultipleAsync` | `InformeVentasAgnoMesEjecutivo` | +| **SP complejo** (cursor, temp tables) | **Dapper** + `CommandType.StoredProcedure` | Reportes Crystal | + +**DbContext (EF Core):** +```csharp +public class VentasDbContext : DbContext +{ + public VentasDbContext(DbContextOptions options) : base(options) { } + + // DbSets solo para entidades que usan FromSqlRaw + public DbSet Leads { get; set; } + public DbSet Contratos { get; set; } + // ... + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Configurar entidades sin tabla (solo para SPs) + modelBuilder.Entity(e => { + e.HasNoKey(); + e.ToView(null); + }); + } +} +``` + +**Repositorio con Dapper (SPs complejos):** +```csharp +public class InformeRepository +{ + private readonly string _connectionString; + + public async Task InformeVentasAgnoMesAsync(int mes, int agno, string tipo) + { + using var connection = new NpgsqlConnection(_connectionString); + using var multi = await connection.QueryMultipleAsync( + "sige_sam_v3.InformeVentasAgnoMes", + new { mes, agno, tipo }, + commandType: CommandType.StoredProcedure + ); + + return new InformeVentasDto + { + Resumen = multi.ReadSingle(), + Detalle = multi.Read().ToList(), + Totales = multi.ReadSingle() + }; + } +} +``` + +**Paquetes NuGet requeridos:** +```xml + + + + + + +``` + +#### 1.4 Ventas.Services — Lógica de Negocio + +Migrar las 28 clases de `Negocio/` a servicios con Inyección de Dependencias. + +```csharp +public class LeadService : ILeadService +{ + private readonly ILeadRepository _leadRepository; + + public LeadService(ILeadRepository leadRepository) + { + _leadRepository = leadRepository; + } + + public async Task> IngresarAsync(LeadCreateDto dto) + { + // Validar datos + // Llamar repositorio + // Aplicar reglas de negocio + } +} +``` + +#### 1.5 Ventas.API — Controladores REST + +Crear 1 endpoint por cada método relevante de las 25 clases de datos + 40 code-behind. + +**Ejemplo LeadController:** +```csharp +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class LeadController : ControllerBase +{ + private readonly ILeadService _leadService; + + public LeadController(ILeadService leadService) + { + _leadService = leadService; + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var result = await _leadService.BuscarIdAsync(id); + return Ok(result); + } + + [HttpGet("buscar")] + public async Task Buscar( + [FromQuery] int ejecutivo, + [FromQuery] int estado, + [FromQuery] int dias) + { + var result = await _leadService.BuscarAsync(ejecutivo, estado, dias); + return Ok(result); + } + + [HttpPost] + public async Task Ingresar(LeadCreateDto dto) + { + var result = await _leadService.IngresarAsync(dto); + return Ok(new { mensaje = result }); + } + + [HttpPost("{id}/actividad")] + public async Task IngresarActividad(int id, ActividadCreateDto dto) + { + var result = await _leadService.IngresarActividadAsync(id, dto); + return Ok(new { mensaje = result }); + } +} +``` + +#### 1.6 Autenticación JWT (reemplazo de FormsAuthentication) + +```csharp +// AuthController.cs +[HttpPost("login")] +public async Task Login(LoginRequest request) +{ + var usuario = await _usuarioService.BuscarAsync(request.Rut, request.Clave); + if (usuario == null) + return Unauthorized(); + + var token = _jwtService.GenerateToken(usuario); + var cookieOptions = new CookieOptions + { + HttpOnly = true, + Secure = true, + SameSite = SameSiteMode.Strict, + Expires = DateTime.UtcNow.AddMinutes(30) + }; + Response.Cookies.Append("SAM_TOKEN", token, cookieOptions); + + return Ok(new { + nombre = usuario.Nombre, + sede = usuario.Sede, + token // también devuelto para apps mobile + }); +} +``` + +**Migración de cookies originales → JWT claims:** +| Cookie original (FormsAuth) | Claims JWT | +|---------------------------|------------| +| `SAM_ID_SEDE` | `claim: sede` | +| `SAM_USUARIO_NOMBRE` | `claim: name` | +| `SAM_USUARIO_ID` | `claim: sub` (rut) | +| `SAM_USUARIO_ID_PASS` | No se migra (inseguro) | + +#### 1.7 Reportes QuestPDF (reemplazo Crystal Reports) + +Cada uno de los 17 reportes Crystal → una clase QuestPDF. + +```csharp +// ReportService.cs +public class ReportService : IReportService +{ + public async Task GenerarContratoPdfAsync(int contratoId) + { + var data = await _contratoRepository.BuscarInfoPdfAsync(contratoId); + // ... mapear a modelo del reporte + + return Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Cm); + page.Header().Element(c => ComposeHeader(c, data)); + page.Content().Element(c => ComposeContent(c, data)); + page.Footer().Element(ComposeFooter); + }); + }).GeneratePdf(); + } + + private void ComposeHeader(IContainer container, ContratoPdfData data) + { + container.Row(r => + { + r.ConstantItem(100).Image("logo.png"); + r.RelativeItem().AlignRight().Text($"Contrato N° {data.Numero}") + .FontSize(16).Bold(); + }); + } +} +``` + +**Endpoint de reportes:** +```csharp +[HttpGet("reportes/contrato/{id}")] +public async Task GetContratoPdf(int id) +{ + var pdf = await _reportService.GenerarContratoPdfAsync(id); + return File(pdf, "application/pdf", $"contrato_{id}.pdf"); +} +``` + +--- + +### FASE 2: SERVICES EXTERNOS — API APARTE (2-3 semanas) + +Proyecto separado `services-externos/` que expone servicios reutilizables. + +#### 2.1 LibreDTE (DTE Chile) + +Migrar `Web/FacturadorDTE/LibreDteConector.cs` → `ServicesExternos.API`. + +```csharp +[ApiController] +[Route("api/dte")] +public class DteController : ControllerBase +{ + [HttpPost("emitir")] + public async Task Emitir(DteEmissionRequest request) + { + // Lógica migrada de LibreDteConector.cs + } + + [HttpGet("estado/{codigo}")] + public async Task ConsultarEstado(string codigo) + { + // Consultar estado DTE + } +} +``` + +#### 2.2 Transbank Webpay Plus + +Migrar `Negocio/PagadorTransbank.cs` + `Datos/TransbankInfo.cs`. + +```csharp +[ApiController] +[Route("api/pagos/transbank")] +public class TransbankController : ControllerBase +{ + [HttpPost("crear-transaccion")] + public async Task CrearTransaccion(TransbankRequest request) + { + // Lógica de creación de transacción Webpay + } + + [HttpPost("confirmar")] + public async Task Confirmar(string token_ws) + { + // Confirmar y guardar voucher + } +} +``` + +#### 2.3 SMTP Email (con MailKit) + +Migrar `Negocio/Mails.cs` usando `MailKit` (reemplaza `SmtpClient` obsoleto). + +```csharp +[ApiController] +[Route("api/email")] +public class EmailController : ControllerBase +{ + [HttpPost("send")] + public async Task Send(EmailRequest request) + { + using var client = new SmtpClient(); + await client.ConnectAsync(_smtpHost, _smtpPort, SecureSocketOptions.StartTls); + await client.AuthenticateAsync(_smtpUser, _smtpPassword); + // Enviar con templates HTML existentes + } +} +``` + +--- + +### FASE 3: FRONTEND NEXT.JS (6-8 semanas) + +#### 3.1 Inicialización + +```bash +npx create-next-app@latest frontend --typescript --tailwind --app +``` + +#### 3.2 Layout Principal (SAM.Master) + +Replicar exactamente el `SAM.Master` actual en `app/layout.tsx`: + +- Sidebar con `boxicons` + FontAwesome +- Logo SAM + favicon +- Header con nombre de usuario + botón cerrar sesión +- Timeout de inactividad de 30 min (misma lógica JS) +- Responsive design (media queries originales) +- Bootstrap 5 via CDN (misma versión) +- Google Fonts Fira Sans + +```tsx +// app/layout.tsx +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + + + +
+ {/* Sidebar idéntica al original */} +
+ +
SAM
+ +
+
    +
  • + + + Dashboard + +
  • + {/* ... resto de links idénticos */} +
+
+
+
{/* Nombre usuario + cerrar sesión */} + {children} +
+
+ + + ); +} +``` + +#### 3.3 Login Page + +```tsx +// app/login/page.tsx +"use client"; +export default function LoginPage() { + const [rut, setRut] = useState(""); + const [clave, setClave] = useState(""); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const res = await api.post("/auth/login", { rut, clave }); + // Guardar token, redirigir a dashboard + }; + + return ( +
+
+
+
+
+

Iniciar Sesión

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