From 52b7a56c695936e8fd3414a196c5ff8f805f58d5 Mon Sep 17 00:00:00 2001 From: edelomeza Date: Wed, 15 Jul 2026 14:30:20 -0600 Subject: [PATCH] feat: add Venta CRUD module with tests and security validation --- WebDevSecOps/Controllers/VentaController.cs | 131 +++++++++++ .../Models/CliClienteAutocompleteDto.cs | 12 + .../Models/SegUsuarioAutocompleteDto.cs | 12 + WebDevSecOps/Models/VenCatEstado.cs | 15 ++ WebDevSecOps/Models/Venta.cs | 36 +++ WebDevSecOps/Models/VentaCreateViewModel.cs | 23 ++ WebDevSecOps/Pages/Shared/_Layout.cshtml | 3 + WebDevSecOps/Program.cs | 16 ++ WebDevSecOps/Services/ClienteService.cs | 38 +++ WebDevSecOps/Services/EstadoVentaService.cs | 109 +++++++++ WebDevSecOps/Services/IClienteService.cs | 2 + WebDevSecOps/Services/IEstadoVentaService.cs | 10 + WebDevSecOps/Services/IUsuarioService.cs | 2 + WebDevSecOps/Services/IVentaService.cs | 12 + WebDevSecOps/Services/UsuarioService.cs | 38 +++ WebDevSecOps/Services/VentaService.cs | 175 ++++++++++++++ WebDevSecOps/Views/Venta/Create.cshtml | 144 ++++++++++++ WebDevSecOps/Views/Venta/Index.cshtml | 112 +++++++++ WebDevSecOps/Views/Venta/Memoria.md | 159 +++++++++++++ WebDevSecOps/appsettings.json | 4 +- .../Services/VentaApiClientTests.cs | 182 +++++++++++++++ .../SAST/VentaSecurityTests.cs | 209 +++++++++++++++++ .../Common/ContractTestData.cs | 83 +++++++ .../Pages/Ventas/CreateTests.cs | 186 +++++++++++++++ .../Pages/Ventas/IndexTests.cs | 220 ++++++++++++++++++ 25 files changed, 1931 insertions(+), 2 deletions(-) create mode 100644 WebDevSecOps/Controllers/VentaController.cs create mode 100644 WebDevSecOps/Models/CliClienteAutocompleteDto.cs create mode 100644 WebDevSecOps/Models/SegUsuarioAutocompleteDto.cs create mode 100644 WebDevSecOps/Models/VenCatEstado.cs create mode 100644 WebDevSecOps/Models/Venta.cs create mode 100644 WebDevSecOps/Models/VentaCreateViewModel.cs create mode 100644 WebDevSecOps/Services/EstadoVentaService.cs create mode 100644 WebDevSecOps/Services/IEstadoVentaService.cs create mode 100644 WebDevSecOps/Services/IVentaService.cs create mode 100644 WebDevSecOps/Services/VentaService.cs create mode 100644 WebDevSecOps/Views/Venta/Create.cshtml create mode 100644 WebDevSecOps/Views/Venta/Index.cshtml create mode 100644 WebDevSecOps/Views/Venta/Memoria.md create mode 100644 tests/WebDevSecOps.IntegrationTests/Services/VentaApiClientTests.cs create mode 100644 tests/WebDevSecOps.SecurityTests/SAST/VentaSecurityTests.cs create mode 100644 tests/WebDevSecOps.UnitTests/Pages/Ventas/CreateTests.cs create mode 100644 tests/WebDevSecOps.UnitTests/Pages/Ventas/IndexTests.cs diff --git a/WebDevSecOps/Controllers/VentaController.cs b/WebDevSecOps/Controllers/VentaController.cs new file mode 100644 index 0000000..338f779 --- /dev/null +++ b/WebDevSecOps/Controllers/VentaController.cs @@ -0,0 +1,131 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using WebDevSecOps.Models; +using WebDevSecOps.Services; + +namespace WebDevSecOps.Controllers; + +[Authorize] +public class VentaController : Controller +{ + private readonly IVentaService _service; + private readonly IEstadoVentaService _estadoVentaService; + private readonly IClienteService _clienteService; + private readonly IUsuarioService _usuarioService; + private readonly ILogger _logger; + + public VentaController(IVentaService service, IEstadoVentaService estadoVentaService, IClienteService clienteService, IUsuarioService usuarioService, ILogger logger) + { + _service = service; + _estadoVentaService = estadoVentaService; + _clienteService = clienteService; + _usuarioService = usuarioService; + _logger = logger; + } + + [HttpGet] + [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Client, NoStore = false)] + public async Task Index(string? texto = null, DateTime? dteFechaInicio = null, DateTime? dteFechaFin = null, int pageNumber = 1, int pageSize = 10, CancellationToken ct = default) + { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + await CargarEstadoVentasAsync(ct); + + var result = (!string.IsNullOrEmpty(texto) || dteFechaInicio.HasValue || dteFechaFin.HasValue) + ? await _service.SearchVentasAsync(texto, dteFechaInicio, dteFechaFin, pageNumber, pageSize, ct) + : await _service.GetVentasAsync(pageNumber, pageSize, ct); + + if (result is null) + { + _logger.LogWarning("Failed to load ventas — service returned null"); + TempData["Error"] = "No se pudieron cargar las ventas."; + } + + return View(result); + } + + [HttpGet] + public async Task Create(string? returnUrl = null, CancellationToken ct = default) + { + ViewData["ReturnUrl"] = returnUrl; + return View(); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(VentaCreateViewModel model, CancellationToken ct, string? returnUrl = null) + { + if (!ModelState.IsValid) + { + ViewData["ReturnUrl"] = returnUrl; + return View(model); + } + + var result = await _service.CreateVentaAsync(model, ct); + + if (result.Success) + { + _logger.LogInformation("Venta created successfully"); + TempData["Success"] = "Venta creada exitosamente."; + if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl)) + return Redirect(returnUrl); + return RedirectToAction("Index"); + } + + if (result.FieldErrors is not null) + { + foreach (var kvp in result.FieldErrors) + { + foreach (var msg in kvp.Value) + { + var key = kvp.Key switch + { + "idCliCliente" => nameof(VentaCreateViewModel.IdCliCliente), + "idSegUsuario" => nameof(VentaCreateViewModel.IdSegUsuario), + _ => kvp.Key + }; + ModelState.AddModelError(key, msg); + } + } + } + + if (result.ErrorMessage is not null) + ModelState.AddModelError(string.Empty, result.ErrorMessage); + + ViewData["ReturnUrl"] = returnUrl; + return View(model); + } + + [HttpGet] + public async Task ClientesAutocomplete(string texto, int maxResultados = 10, CancellationToken ct = default) + { + if (!ModelState.IsValid) + return Json(new List()); + + if (string.IsNullOrWhiteSpace(texto) || texto.Length < 2) + return Json(new List()); + + var result = await _clienteService.AutocompleteClientesAsync(texto, maxResultados, ct); + return Json(result ?? []); + } + + [HttpGet] + public async Task UsuariosAutocomplete(string texto, int maxResultados = 10, CancellationToken ct = default) + { + if (!ModelState.IsValid) + return Json(new List()); + + if (string.IsNullOrWhiteSpace(texto) || texto.Length < 2) + return Json(new List()); + + var result = await _usuarioService.AutocompleteUsuariosAsync(texto, maxResultados, ct); + return Json(result ?? []); + } + + private async Task CargarEstadoVentasAsync(CancellationToken ct = default) + { + var estados = await _estadoVentaService.GetAllAsync(ct: ct); + ViewBag.EstadoVentaDict = estados?.Items?.ToDictionary(t => t.Id, t => t.StrValor) ?? []; + } +} diff --git a/WebDevSecOps/Models/CliClienteAutocompleteDto.cs b/WebDevSecOps/Models/CliClienteAutocompleteDto.cs new file mode 100644 index 0000000..ef1b41e --- /dev/null +++ b/WebDevSecOps/Models/CliClienteAutocompleteDto.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace WebDevSecOps.Models; + +public class CliClienteAutocompleteDto +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("strNombreCliente")] + public string StrNombreCliente { get; set; } = string.Empty; +} diff --git a/WebDevSecOps/Models/SegUsuarioAutocompleteDto.cs b/WebDevSecOps/Models/SegUsuarioAutocompleteDto.cs new file mode 100644 index 0000000..a6c0423 --- /dev/null +++ b/WebDevSecOps/Models/SegUsuarioAutocompleteDto.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace WebDevSecOps.Models; + +public class SegUsuarioAutocompleteDto +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("strNombre")] + public string StrNombre { get; set; } = string.Empty; +} diff --git a/WebDevSecOps/Models/VenCatEstado.cs b/WebDevSecOps/Models/VenCatEstado.cs new file mode 100644 index 0000000..f96cbf9 --- /dev/null +++ b/WebDevSecOps/Models/VenCatEstado.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace WebDevSecOps.Models; + +public class VenCatEstado +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("strValor")] + public string StrValor { get; set; } = string.Empty; + + [JsonPropertyName("strDescripcion")] + public string? StrDescripcion { get; set; } +} diff --git a/WebDevSecOps/Models/Venta.cs b/WebDevSecOps/Models/Venta.cs new file mode 100644 index 0000000..c10fe66 --- /dev/null +++ b/WebDevSecOps/Models/Venta.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace WebDevSecOps.Models; + +public class Venta +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("idCliCliente")] + public int IdCliCliente { get; set; } + + [JsonPropertyName("strNombreCliente")] + public string? StrNombreCliente { get; set; } + + [JsonPropertyName("idSegUsuario")] + public int IdSegUsuario { get; set; } + + [JsonPropertyName("strNombreUsuario")] + public string? StrNombreUsuario { get; set; } + + [JsonPropertyName("idVenCatEstado")] + public int IdVenCatEstado { get; set; } + + [JsonPropertyName("strEstado")] + public string? StrEstado { get; set; } + + [JsonPropertyName("dteFechaHoraCompra")] + public DateTime? DteFechaHoraCompra { get; set; } + + [JsonPropertyName("strClaveVenta")] + public string StrClaveVenta { get; set; } = string.Empty; + + [JsonPropertyName("rowVersion")] + public byte[]? RowVersion { get; set; } +} diff --git a/WebDevSecOps/Models/VentaCreateViewModel.cs b/WebDevSecOps/Models/VentaCreateViewModel.cs new file mode 100644 index 0000000..613e777 --- /dev/null +++ b/WebDevSecOps/Models/VentaCreateViewModel.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace WebDevSecOps.Models; + +public class VentaCreateViewModel +{ + [Required(ErrorMessage = "El cliente es obligatorio.")] + [JsonRequired] + [JsonPropertyName("idCliCliente")] + [Display(Name = "Cliente")] + public int IdCliCliente { get; set; } + + [Required(ErrorMessage = "El usuario es obligatorio.")] + [JsonRequired] + [JsonPropertyName("idSegUsuario")] + [Display(Name = "Usuario")] + public int IdSegUsuario { get; set; } + + public string? StrNombreCliente { get; set; } + + public string? StrNombreUsuario { get; set; } +} diff --git a/WebDevSecOps/Pages/Shared/_Layout.cshtml b/WebDevSecOps/Pages/Shared/_Layout.cshtml index 610f1ff..5ac028d 100644 --- a/WebDevSecOps/Pages/Shared/_Layout.cshtml +++ b/WebDevSecOps/Pages/Shared/_Layout.cshtml @@ -38,6 +38,9 @@ + }