From 1732a678685abdb8e6d5c7e824724524342920b7 Mon Sep 17 00:00:00 2001 From: Diego Leal Date: Mon, 9 Feb 2026 18:48:51 -0300 Subject: [PATCH 01/13] initial --- EcommerceApi.slnx | 3 +++ .../Controllers/WeatherForecastController.cs | 26 +++++++++++++++++++ EcommerceApi/EcommerceApi.csproj | 13 ++++++++++ EcommerceApi/EcommerceApi.http | 6 +++++ EcommerceApi/Models/Category.cs | 9 +++++++ EcommerceApi/Models/Product.cs | 11 ++++++++ EcommerceApi/Models/Sale.cs | 8 ++++++ EcommerceApi/Program.cs | 25 ++++++++++++++++++ EcommerceApi/Properties/launchSettings.json | 23 ++++++++++++++++ EcommerceApi/WeatherForecast.cs | 13 ++++++++++ EcommerceApi/appsettings.Development.json | 8 ++++++ EcommerceApi/appsettings.json | 12 +++++++++ 12 files changed, 157 insertions(+) create mode 100644 EcommerceApi.slnx create mode 100644 EcommerceApi/Controllers/WeatherForecastController.cs create mode 100644 EcommerceApi/EcommerceApi.csproj create mode 100644 EcommerceApi/EcommerceApi.http create mode 100644 EcommerceApi/Models/Category.cs create mode 100644 EcommerceApi/Models/Product.cs create mode 100644 EcommerceApi/Models/Sale.cs create mode 100644 EcommerceApi/Program.cs create mode 100644 EcommerceApi/Properties/launchSettings.json create mode 100644 EcommerceApi/WeatherForecast.cs create mode 100644 EcommerceApi/appsettings.Development.json create mode 100644 EcommerceApi/appsettings.json diff --git a/EcommerceApi.slnx b/EcommerceApi.slnx new file mode 100644 index 00000000..b768a73c --- /dev/null +++ b/EcommerceApi.slnx @@ -0,0 +1,3 @@ + + + diff --git a/EcommerceApi/Controllers/WeatherForecastController.cs b/EcommerceApi/Controllers/WeatherForecastController.cs new file mode 100644 index 00000000..a03c0ba3 --- /dev/null +++ b/EcommerceApi/Controllers/WeatherForecastController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; + +namespace EcommerceApi.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = + [ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + ]; + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } + } +} diff --git a/EcommerceApi/EcommerceApi.csproj b/EcommerceApi/EcommerceApi.csproj new file mode 100644 index 00000000..530dd4e7 --- /dev/null +++ b/EcommerceApi/EcommerceApi.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/EcommerceApi/EcommerceApi.http b/EcommerceApi/EcommerceApi.http new file mode 100644 index 00000000..c7ce83ac --- /dev/null +++ b/EcommerceApi/EcommerceApi.http @@ -0,0 +1,6 @@ +@EcommerceApi_HostAddress = http://localhost:5115 + +GET {{EcommerceApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/EcommerceApi/Models/Category.cs b/EcommerceApi/Models/Category.cs new file mode 100644 index 00000000..98abbd2d --- /dev/null +++ b/EcommerceApi/Models/Category.cs @@ -0,0 +1,9 @@ +namespace EcommerceApi.Models; + +public class Category +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public bool IsDeleted { get; set; } = false; + public ICollection Products { get; set; } = new List(); +} diff --git a/EcommerceApi/Models/Product.cs b/EcommerceApi/Models/Product.cs new file mode 100644 index 00000000..e7669647 --- /dev/null +++ b/EcommerceApi/Models/Product.cs @@ -0,0 +1,11 @@ +namespace EcommerceApi.Models; + +public class Product +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public int CategoryId { get; set; } + public Category Category { get; set; } = null!; + public ICollection Sales { get; set; } = []; +} diff --git a/EcommerceApi/Models/Sale.cs b/EcommerceApi/Models/Sale.cs new file mode 100644 index 00000000..dd5fe5b8 --- /dev/null +++ b/EcommerceApi/Models/Sale.cs @@ -0,0 +1,8 @@ +namespace EcommerceApi.Models; + +public class Sale +{ + public int Id { get; set; } + public DateTime SaleDate { get; set; } + public ICollection Products { get; set; } = []; +} diff --git a/EcommerceApi/Program.cs b/EcommerceApi/Program.cs new file mode 100644 index 00000000..8f83df52 --- /dev/null +++ b/EcommerceApi/Program.cs @@ -0,0 +1,25 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +//builder.Services.AddDbContext(options => +// options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")) +// .UseAsyncSeeding(async (context, _, CancellationToken) => +// { +// await DatabaseSeeding.CustomSeeding((ShiftsLoggerContext)context); +// }) +// ); + +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); diff --git a/EcommerceApi/Properties/launchSettings.json b/EcommerceApi/Properties/launchSettings.json new file mode 100644 index 00000000..b28bb834 --- /dev/null +++ b/EcommerceApi/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5115", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7216;http://localhost:5115", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/EcommerceApi/WeatherForecast.cs b/EcommerceApi/WeatherForecast.cs new file mode 100644 index 00000000..d3cb9368 --- /dev/null +++ b/EcommerceApi/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace EcommerceApi +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/EcommerceApi/appsettings.Development.json b/EcommerceApi/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/EcommerceApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/EcommerceApi/appsettings.json b/EcommerceApi/appsettings.json new file mode 100644 index 00000000..1e18fc9b --- /dev/null +++ b/EcommerceApi/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CommerceDB;Trusted_Connection=True;TrustServerCertificate=True;" + } +} From f7a8ec4ac9e47d1262215291117bdfe80bce1fb0 Mon Sep 17 00:00:00 2001 From: Diego Leal Date: Mon, 9 Feb 2026 18:49:37 -0300 Subject: [PATCH 02/13] change folder name --- .../Controllers/WeatherForecastController.cs | 0 {EcommerceApi => EcommerceApi.DiegoPetrola}/EcommerceApi.csproj | 0 {EcommerceApi => EcommerceApi.DiegoPetrola}/EcommerceApi.http | 0 {EcommerceApi => EcommerceApi.DiegoPetrola}/Models/Category.cs | 0 {EcommerceApi => EcommerceApi.DiegoPetrola}/Models/Product.cs | 0 {EcommerceApi => EcommerceApi.DiegoPetrola}/Models/Sale.cs | 0 {EcommerceApi => EcommerceApi.DiegoPetrola}/Program.cs | 0 .../Properties/launchSettings.json | 0 {EcommerceApi => EcommerceApi.DiegoPetrola}/WeatherForecast.cs | 0 .../appsettings.Development.json | 0 {EcommerceApi => EcommerceApi.DiegoPetrola}/appsettings.json | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename {EcommerceApi => EcommerceApi.DiegoPetrola}/Controllers/WeatherForecastController.cs (100%) rename {EcommerceApi => EcommerceApi.DiegoPetrola}/EcommerceApi.csproj (100%) rename {EcommerceApi => EcommerceApi.DiegoPetrola}/EcommerceApi.http (100%) rename {EcommerceApi => EcommerceApi.DiegoPetrola}/Models/Category.cs (100%) rename {EcommerceApi => EcommerceApi.DiegoPetrola}/Models/Product.cs (100%) rename {EcommerceApi => EcommerceApi.DiegoPetrola}/Models/Sale.cs (100%) rename {EcommerceApi => EcommerceApi.DiegoPetrola}/Program.cs (100%) rename {EcommerceApi => EcommerceApi.DiegoPetrola}/Properties/launchSettings.json (100%) rename {EcommerceApi => EcommerceApi.DiegoPetrola}/WeatherForecast.cs (100%) rename {EcommerceApi => EcommerceApi.DiegoPetrola}/appsettings.Development.json (100%) rename {EcommerceApi => EcommerceApi.DiegoPetrola}/appsettings.json (100%) diff --git a/EcommerceApi/Controllers/WeatherForecastController.cs b/EcommerceApi.DiegoPetrola/Controllers/WeatherForecastController.cs similarity index 100% rename from EcommerceApi/Controllers/WeatherForecastController.cs rename to EcommerceApi.DiegoPetrola/Controllers/WeatherForecastController.cs diff --git a/EcommerceApi/EcommerceApi.csproj b/EcommerceApi.DiegoPetrola/EcommerceApi.csproj similarity index 100% rename from EcommerceApi/EcommerceApi.csproj rename to EcommerceApi.DiegoPetrola/EcommerceApi.csproj diff --git a/EcommerceApi/EcommerceApi.http b/EcommerceApi.DiegoPetrola/EcommerceApi.http similarity index 100% rename from EcommerceApi/EcommerceApi.http rename to EcommerceApi.DiegoPetrola/EcommerceApi.http diff --git a/EcommerceApi/Models/Category.cs b/EcommerceApi.DiegoPetrola/Models/Category.cs similarity index 100% rename from EcommerceApi/Models/Category.cs rename to EcommerceApi.DiegoPetrola/Models/Category.cs diff --git a/EcommerceApi/Models/Product.cs b/EcommerceApi.DiegoPetrola/Models/Product.cs similarity index 100% rename from EcommerceApi/Models/Product.cs rename to EcommerceApi.DiegoPetrola/Models/Product.cs diff --git a/EcommerceApi/Models/Sale.cs b/EcommerceApi.DiegoPetrola/Models/Sale.cs similarity index 100% rename from EcommerceApi/Models/Sale.cs rename to EcommerceApi.DiegoPetrola/Models/Sale.cs diff --git a/EcommerceApi/Program.cs b/EcommerceApi.DiegoPetrola/Program.cs similarity index 100% rename from EcommerceApi/Program.cs rename to EcommerceApi.DiegoPetrola/Program.cs diff --git a/EcommerceApi/Properties/launchSettings.json b/EcommerceApi.DiegoPetrola/Properties/launchSettings.json similarity index 100% rename from EcommerceApi/Properties/launchSettings.json rename to EcommerceApi.DiegoPetrola/Properties/launchSettings.json diff --git a/EcommerceApi/WeatherForecast.cs b/EcommerceApi.DiegoPetrola/WeatherForecast.cs similarity index 100% rename from EcommerceApi/WeatherForecast.cs rename to EcommerceApi.DiegoPetrola/WeatherForecast.cs diff --git a/EcommerceApi/appsettings.Development.json b/EcommerceApi.DiegoPetrola/appsettings.Development.json similarity index 100% rename from EcommerceApi/appsettings.Development.json rename to EcommerceApi.DiegoPetrola/appsettings.Development.json diff --git a/EcommerceApi/appsettings.json b/EcommerceApi.DiegoPetrola/appsettings.json similarity index 100% rename from EcommerceApi/appsettings.json rename to EcommerceApi.DiegoPetrola/appsettings.json From 178f059214d2da8a8c2d4a0d6400bfe9e6168ea7 Mon Sep 17 00:00:00 2001 From: Diego Leal Date: Mon, 9 Feb 2026 20:42:26 -0300 Subject: [PATCH 03/13] added stub category controllers and service --- .../Context/EcommerceDbContext.cs | 42 +++++++++++++ .../Controllers/CategoriesController.cs | 32 ++++++++++ .../Controllers/ProductsController.cs | 40 ++++++++++++ .../Controllers/SalesController.cs | 62 +++++++++++++++++++ .../Controllers/WeatherForecastController.cs | 26 -------- EcommerceApi.DiegoPetrola/EcommerceApi.csproj | 2 + EcommerceApi.DiegoPetrola/Models/Category.cs | 2 +- .../Models/DTOs/CommerceDtos.cs | 10 +++ EcommerceApi.DiegoPetrola/Models/Product.cs | 2 +- EcommerceApi.DiegoPetrola/Models/Sale.cs | 2 +- EcommerceApi.DiegoPetrola/Models/SaleItem.cs | 10 +++ EcommerceApi.DiegoPetrola/Program.cs | 18 +++--- .../Properties/launchSettings.json | 4 +- .../Services/CategoriesService.cs | 37 +++++++++++ EcommerceApi.DiegoPetrola/WeatherForecast.cs | 13 ---- EcommerceApi.slnx | 2 +- 16 files changed, 252 insertions(+), 52 deletions(-) create mode 100644 EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs create mode 100644 EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs create mode 100644 EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs create mode 100644 EcommerceApi.DiegoPetrola/Controllers/SalesController.cs delete mode 100644 EcommerceApi.DiegoPetrola/Controllers/WeatherForecastController.cs create mode 100644 EcommerceApi.DiegoPetrola/Models/DTOs/CommerceDtos.cs create mode 100644 EcommerceApi.DiegoPetrola/Models/SaleItem.cs create mode 100644 EcommerceApi.DiegoPetrola/Services/CategoriesService.cs delete mode 100644 EcommerceApi.DiegoPetrola/WeatherForecast.cs diff --git a/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs b/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs new file mode 100644 index 00000000..74d022b8 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs @@ -0,0 +1,42 @@ +using EcommerceApi.Models; +using Microsoft.EntityFrameworkCore; + +namespace EcommerceApi.Context; + +public class EcommerceDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Sales { get; set; } + public DbSet SaleItems { get; set; } + public DbSet Products { get; set; } + public DbSet Categories { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasMany(s => s.SaleItems) + .WithOne(si => si.Sale) + .HasForeignKey(si => si.SaleId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasOne(p => p.Category) + .WithMany(c => c.Products) + .HasForeignKey(p => p.CategoryId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasMany(p => p.SaleItems) + .WithOne(si => si.Product) + .HasForeignKey(si => si.ProductId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasKey(si => new { si.SaleId, si.ProductId }); + + modelBuilder.Entity() + .HasIndex(c => c.Name) + .IsUnique(); + } +} \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs b/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs new file mode 100644 index 00000000..e696d327 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs @@ -0,0 +1,32 @@ +using EcommerceApi.Models.DTOs; +using EcommerceApi.Services; +using Microsoft.AspNetCore.Mvc; + +namespace EcommerceApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class CategoriesController(CategoriesService service) : ControllerBase +{ + [HttpGet] + public async Task>> GetCategories() + { + var categories = await service.GetCategories(); + return Ok(categories); + } + + [HttpPost] + public async Task> CreateCategory(CreateCategoryDto dto) + { + var newDto = service.CreateCategory(dto); + return CreatedAtAction(nameof(GetCategories), new { id = newDto.Id }, newDto); + } + + [HttpDelete("{id}")] + public async Task DeleteCategory(int id) + { + // TODO: implement result pattern + await service.SoftDeleteCategory(id); + return NoContent(); + } +} diff --git a/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs b/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs new file mode 100644 index 00000000..51941d16 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs @@ -0,0 +1,40 @@ +using EcommerceApi.Context; +using EcommerceApi.Models; +using EcommerceApi.Models.DTOs; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace EcommerceApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ProductsController(EcommerceDbContext context) : ControllerBase +{ + [HttpGet] + public async Task>> GetProducts() + { + return await context.Products + .Include(p => p.Category) + .Select(p => new ProductDto(p.Id, p.Name, p.Price, p.CategoryId, p.Category.Name)) + .ToListAsync(); + } + + [HttpPost] + public async Task> CreateProduct(CreateProductDto dto) + { + var categoryExists = await context.Categories.AnyAsync(c => c.Id == dto.CategoryId); + if (!categoryExists) return BadRequest("Invalid Category ID"); + + var product = new Product + { + Name = dto.Name, + Price = dto.Price, + CategoryId = dto.CategoryId + }; + + context.Products.Add(product); + await context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetProducts), new { id = product.Id }, dto); + } +} diff --git a/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs b/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs new file mode 100644 index 00000000..a0e821be --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs @@ -0,0 +1,62 @@ +using EcommerceApi.Context; +using EcommerceApi.Models; +using EcommerceApi.Models.DTOs; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace EcommerceApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class SalesController(EcommerceDbContext context) : ControllerBase +{ + [HttpGet("{id}")] + public async Task> GetSale(int id) + { + var sale = await context.Sales + .Include(s => s.SaleItems) + .ThenInclude(si => si.Product) + .FirstOrDefaultAsync(s => s.Id == id); + + if (sale is null) return NotFound(); + + var dto = new SaleDto( + sale.Id, + sale.SaleDate, + [.. sale.SaleItems.Select(si => new SaleItemDto( + si.ProductId, + si.Product.Name, + si.Quantity, + si.Product.Price))], + sale.SaleItems.Sum(si => si.Quantity * si.Product.Price) + ); + + return Ok(dto); + } + + [HttpPost] + public async Task> CreateSale(CreateSaleDto dto) + { + if (dto.Items == null || dto.Items.Count == 0) + return BadRequest("Sale must contain at least one item."); + + var sale = new Sale { SaleDate = DateTime.UtcNow }; + + foreach (var item in dto.Items) + { + var product = await context.Products.FindAsync(item.ProductId); + if (product == null) return BadRequest($"Product {item.ProductId} not found."); + + sale.SaleItems.Add(new SaleItem + { + ProductId = item.ProductId, + Quantity = item.Quantity + }); + } + + context.Sales.Add(sale); + await context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetSale), new { id = sale.Id }, sale.Id); + } +} \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/Controllers/WeatherForecastController.cs b/EcommerceApi.DiegoPetrola/Controllers/WeatherForecastController.cs deleted file mode 100644 index a03c0ba3..00000000 --- a/EcommerceApi.DiegoPetrola/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace EcommerceApi.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = - [ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - ]; - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/EcommerceApi.DiegoPetrola/EcommerceApi.csproj b/EcommerceApi.DiegoPetrola/EcommerceApi.csproj index 530dd4e7..dfcdd877 100644 --- a/EcommerceApi.DiegoPetrola/EcommerceApi.csproj +++ b/EcommerceApi.DiegoPetrola/EcommerceApi.csproj @@ -8,6 +8,8 @@ + + diff --git a/EcommerceApi.DiegoPetrola/Models/Category.cs b/EcommerceApi.DiegoPetrola/Models/Category.cs index 98abbd2d..cc98184c 100644 --- a/EcommerceApi.DiegoPetrola/Models/Category.cs +++ b/EcommerceApi.DiegoPetrola/Models/Category.cs @@ -5,5 +5,5 @@ public class Category public int Id { get; set; } public string Name { get; set; } = string.Empty; public bool IsDeleted { get; set; } = false; - public ICollection Products { get; set; } = new List(); + public ICollection Products { get; set; } = []; } diff --git a/EcommerceApi.DiegoPetrola/Models/DTOs/CommerceDtos.cs b/EcommerceApi.DiegoPetrola/Models/DTOs/CommerceDtos.cs new file mode 100644 index 00000000..cd90e0f3 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Models/DTOs/CommerceDtos.cs @@ -0,0 +1,10 @@ +namespace EcommerceApi.Models.DTOs; + +public record CategoryDto(int Id, string Name, bool IsDeleted); +public record CreateCategoryDto(string Name); +public record ProductDto(int Id, string Name, decimal Price, int CategoryId, string CategoryName); +public record CreateProductDto(string Name, decimal Price, int CategoryId); +public record SaleItemDto(int ProductId, string ProductName, int Quantity, decimal UnitPrice); +public record SaleDto(int Id, DateTime SaleDate, List Items, decimal TotalAmount); +public record CreateSaleDto(List Items); +public record CreateSaleItemDto(int ProductId, int Quantity); diff --git a/EcommerceApi.DiegoPetrola/Models/Product.cs b/EcommerceApi.DiegoPetrola/Models/Product.cs index e7669647..cff1cd06 100644 --- a/EcommerceApi.DiegoPetrola/Models/Product.cs +++ b/EcommerceApi.DiegoPetrola/Models/Product.cs @@ -7,5 +7,5 @@ public class Product public decimal Price { get; set; } public int CategoryId { get; set; } public Category Category { get; set; } = null!; - public ICollection Sales { get; set; } = []; + public ICollection SaleItems { get; set; } = []; } diff --git a/EcommerceApi.DiegoPetrola/Models/Sale.cs b/EcommerceApi.DiegoPetrola/Models/Sale.cs index dd5fe5b8..f7657841 100644 --- a/EcommerceApi.DiegoPetrola/Models/Sale.cs +++ b/EcommerceApi.DiegoPetrola/Models/Sale.cs @@ -4,5 +4,5 @@ public class Sale { public int Id { get; set; } public DateTime SaleDate { get; set; } - public ICollection Products { get; set; } = []; + public ICollection SaleItems { get; set; } = []; } diff --git a/EcommerceApi.DiegoPetrola/Models/SaleItem.cs b/EcommerceApi.DiegoPetrola/Models/SaleItem.cs new file mode 100644 index 00000000..17f385e5 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Models/SaleItem.cs @@ -0,0 +1,10 @@ +namespace EcommerceApi.Models; + +public class SaleItem +{ + public int ProductId { get; set; } + public Product Product { get; set; } = null!; + public int SaleId { get; set; } + public Sale Sale { get; set; } = null!; + public int Quantity { get; set; } +} \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/Program.cs b/EcommerceApi.DiegoPetrola/Program.cs index 8f83df52..c07cfa17 100644 --- a/EcommerceApi.DiegoPetrola/Program.cs +++ b/EcommerceApi.DiegoPetrola/Program.cs @@ -1,13 +1,17 @@ +using EcommerceApi.Context; +using Microsoft.EntityFrameworkCore; + var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); -//builder.Services.AddDbContext(options => -// options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")) -// .UseAsyncSeeding(async (context, _, CancellationToken) => -// { -// await DatabaseSeeding.CustomSeeding((ShiftsLoggerContext)context); -// }) -// ); +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")) + .UseAsyncSeeding(async (context, _, CancellationToken) => + { + // TODO generate seeding later + //await DatabaseSeeding.CustomSeeding((EcommerceDbContext)context); + }) + ); builder.Services.AddOpenApi(); diff --git a/EcommerceApi.DiegoPetrola/Properties/launchSettings.json b/EcommerceApi.DiegoPetrola/Properties/launchSettings.json index b28bb834..8d908ba7 100644 --- a/EcommerceApi.DiegoPetrola/Properties/launchSettings.json +++ b/EcommerceApi.DiegoPetrola/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "http://localhost:5115", + "applicationUrl": "http://localhost:5112", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -14,7 +14,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://localhost:7216;http://localhost:5115", + "applicationUrl": "https://localhost:7216;http://localhost:5112", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs new file mode 100644 index 00000000..289c2214 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs @@ -0,0 +1,37 @@ +using EcommerceApi.Context; +using EcommerceApi.Models; +using EcommerceApi.Models.DTOs; +using Microsoft.EntityFrameworkCore; + +namespace EcommerceApi.Services; + +public class CategoriesService(EcommerceDbContext context) +{ + public async Task> GetCategories() + { + var categories = await context.Categories + .Where(c => !c.IsDeleted) + .Select(c => new CategoryDto(c.Id, c.Name, c.IsDeleted)) + .ToListAsync(); + return categories; + } + + public async Task SoftDeleteCategory(int id) + { + var category = await context.Categories.FindAsync(id); + if (category is null) return; + + category.IsDeleted = true; + await context.SaveChangesAsync(); + return; + } + + public async Task CreateCategory(CreateCategoryDto dto) + { + var category = new Category { Name = dto.Name }; + context.Categories.Add(category); + await context.SaveChangesAsync(); + + return new CategoryDto(category.Id, category.Name, category.IsDeleted); + } +} diff --git a/EcommerceApi.DiegoPetrola/WeatherForecast.cs b/EcommerceApi.DiegoPetrola/WeatherForecast.cs deleted file mode 100644 index d3cb9368..00000000 --- a/EcommerceApi.DiegoPetrola/WeatherForecast.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace EcommerceApi -{ - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } - } -} diff --git a/EcommerceApi.slnx b/EcommerceApi.slnx index b768a73c..00d3749f 100644 --- a/EcommerceApi.slnx +++ b/EcommerceApi.slnx @@ -1,3 +1,3 @@ - + From 2b419b2821d86feb856d83ab420bb1a715fed4c0 Mon Sep 17 00:00:00 2001 From: Diego Leal Date: Thu, 12 Feb 2026 02:10:23 -0300 Subject: [PATCH 04/13] Product controller and service --- .../Context/EcommerceDbContext.cs | 4 + .../Controllers/CategoriesController.cs | 38 ++++- .../Controllers/ProductsController.cs | 58 ++++--- EcommerceApi.DiegoPetrola/EcommerceApi.csproj | 12 +- EcommerceApi.DiegoPetrola/EcommerceApi.http | 22 ++- EcommerceApi.DiegoPetrola/Errors/Errors.cs | 3 + .../20260209234942_Init.Designer.cs | 158 +++++++++++++++++ .../Migrations/20260209234942_Init.cs | 120 +++++++++++++ ...12050849_Is_Delete_for_Product.Designer.cs | 161 ++++++++++++++++++ .../20260212050849_Is_Delete_for_Product.cs | 29 ++++ .../EcommerceDbContextModelSnapshot.cs | 158 +++++++++++++++++ EcommerceApi.DiegoPetrola/Models/Product.cs | 1 + EcommerceApi.DiegoPetrola/Program.cs | 17 +- .../Results/GlobalExceptionHandler.cs | 40 +++++ EcommerceApi.DiegoPetrola/Results/Result.cs | 42 +++++ .../Services/CategoriesService.cs | 37 +++- .../Services/ProductsService.cs | 68 ++++++++ 17 files changed, 912 insertions(+), 56 deletions(-) create mode 100644 EcommerceApi.DiegoPetrola/Errors/Errors.cs create mode 100644 EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.Designer.cs create mode 100644 EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.cs create mode 100644 EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.Designer.cs create mode 100644 EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.cs create mode 100644 EcommerceApi.DiegoPetrola/Migrations/EcommerceDbContextModelSnapshot.cs create mode 100644 EcommerceApi.DiegoPetrola/Results/GlobalExceptionHandler.cs create mode 100644 EcommerceApi.DiegoPetrola/Results/Result.cs create mode 100644 EcommerceApi.DiegoPetrola/Services/ProductsService.cs diff --git a/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs b/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs index 74d022b8..e544364f 100644 --- a/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs +++ b/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs @@ -32,6 +32,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(si => si.ProductId) .OnDelete(DeleteBehavior.Restrict); + modelBuilder.Entity() + .Property(p => p.Price) + .HasPrecision(18, 2); + modelBuilder.Entity() .HasKey(si => new { si.SaleId, si.ProductId }); diff --git a/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs b/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs index e696d327..b3e483f7 100644 --- a/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs +++ b/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs @@ -1,4 +1,5 @@ using EcommerceApi.Models.DTOs; +using EcommerceApi.Results; using EcommerceApi.Services; using Microsoft.AspNetCore.Mvc; @@ -11,22 +12,45 @@ public class CategoriesController(CategoriesService service) : ControllerBase [HttpGet] public async Task>> GetCategories() { - var categories = await service.GetCategories(); - return Ok(categories); + var res = await service.GetCategories(); + if (!res.IsSuccess) + return Problem(res.Error.Error); + return Ok(res.Value); + } + + [HttpGet("{id}")] + public async Task> GetCategory(int id) + { + var res = await service.GetCategory(id); + if (!res.IsSuccess) + switch (res.Error.ErrorType) + { + case (ErrorType.NotFound): + return NotFound(res.Error.Error); + default: + return Problem(res.Error.Error); + } + + return Ok(res.Value); } [HttpPost] public async Task> CreateCategory(CreateCategoryDto dto) { - var newDto = service.CreateCategory(dto); - return CreatedAtAction(nameof(GetCategories), new { id = newDto.Id }, newDto); + var res = await service.CreateCategory(dto); + if (!res.IsSuccess) + return BadRequest(res.Error.Error); + + return Ok(res.Value); } [HttpDelete("{id}")] - public async Task DeleteCategory(int id) + public async Task DeleteCategory(int id) { - // TODO: implement result pattern - await service.SoftDeleteCategory(id); + var res = await service.SoftDeleteCategory(id); + if (!res.IsSuccess) + return Problem(res.Error.Error); + return NoContent(); } } diff --git a/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs b/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs index 51941d16..e91f8aa0 100644 --- a/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs +++ b/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs @@ -1,40 +1,50 @@ -using EcommerceApi.Context; -using EcommerceApi.Models; -using EcommerceApi.Models.DTOs; +using EcommerceApi.Models.DTOs; +using EcommerceApi.Results; +using EcommerceApi.Services; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; namespace EcommerceApi.Controllers; [ApiController] [Route("api/[controller]")] -public class ProductsController(EcommerceDbContext context) : ControllerBase +public class ProductsController(ProductsService service) : ControllerBase { - [HttpGet] - public async Task>> GetProducts() + [HttpGet("page/{page}")] + public async Task>> GetProducts(int page) { - return await context.Products - .Include(p => p.Category) - .Select(p => new ProductDto(p.Id, p.Name, p.Price, p.CategoryId, p.Category.Name)) - .ToListAsync(); + var res = await service.GetProducts(page); + return Ok(res.Value); } - [HttpPost] - public async Task> CreateProduct(CreateProductDto dto) + [HttpGet("{id}")] + public async Task> GetProductById(int id) { - var categoryExists = await context.Categories.AnyAsync(c => c.Id == dto.CategoryId); - if (!categoryExists) return BadRequest("Invalid Category ID"); + var res = await service.GetProduct(id); + if (!res.IsSuccess) + return res.Error.ErrorType switch + { + (ErrorType.NotFound) => NotFound(res.Error.Error), + _ => Problem(res.Error.Error), + }; - var product = new Product - { - Name = dto.Name, - Price = dto.Price, - CategoryId = dto.CategoryId - }; + return Ok(res.Value); + } - context.Products.Add(product); - await context.SaveChangesAsync(); + [HttpPost] + public async Task> CreateProduct(CreateProductDto dto) + { + var res = await service.CreateProduct(dto); + if (!res.IsSuccess) + return BadRequest(res.Error.Error); + return Ok(res.Value); + } - return CreatedAtAction(nameof(GetProducts), new { id = product.Id }, dto); + [HttpDelete("{id}")] + public async Task> DeleteProduct(int id) + { + var res = await service.SoftDeleteProduct(id); + if (!res.IsSuccess) + return NotFound(res.Error.Error); + return Ok(res.Value); } } diff --git a/EcommerceApi.DiegoPetrola/EcommerceApi.csproj b/EcommerceApi.DiegoPetrola/EcommerceApi.csproj index dfcdd877..570457c6 100644 --- a/EcommerceApi.DiegoPetrola/EcommerceApi.csproj +++ b/EcommerceApi.DiegoPetrola/EcommerceApi.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -7,9 +7,13 @@ - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/EcommerceApi.DiegoPetrola/EcommerceApi.http b/EcommerceApi.DiegoPetrola/EcommerceApi.http index c7ce83ac..225b259a 100644 --- a/EcommerceApi.DiegoPetrola/EcommerceApi.http +++ b/EcommerceApi.DiegoPetrola/EcommerceApi.http @@ -1,6 +1,22 @@ -@EcommerceApi_HostAddress = http://localhost:5115 +@EcommerceApi_HostAddress = http://localhost:5112 -GET {{EcommerceApi_HostAddress}}/weatherforecast/ +GET {{EcommerceApi_HostAddress}}/api/categories/ Accept: application/json +### GET +GET {{EcommerceApi_HostAddress}}/api/categories/1 +Accept: application/json +### CREATE +POST {{EcommerceApi_HostAddress}}/api/categories +Accept: application/json +Content-Type: application/json -### +{ + "Name":"Cat" +} +### GET PRODUCTS +GET {{EcommerceApi_HostAddress}}/api/products/page/1 +Accept: application/json +### GET PRODUCTS +GET {{EcommerceApi_HostAddress}}/api/products/1 +Accept: application/json +### \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/Errors/Errors.cs b/EcommerceApi.DiegoPetrola/Errors/Errors.cs new file mode 100644 index 00000000..bfe6ebb5 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Errors/Errors.cs @@ -0,0 +1,3 @@ +namespace EcommerceApi.Errors; + +public class NotFoundException(string message) : Exception(message) { } diff --git a/EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.Designer.cs b/EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.Designer.cs new file mode 100644 index 00000000..e3c65848 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.Designer.cs @@ -0,0 +1,158 @@ +// +using System; +using EcommerceApi.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EcommerceApi.Migrations +{ + [DbContext(typeof(EcommerceDbContext))] + [Migration("20260209234942_Init")] + partial class Init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("EcommerceApi.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("SaleDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Sales"); + }); + + modelBuilder.Entity("EcommerceApi.Models.SaleItem", b => + { + b.Property("SaleId") + .HasColumnType("int"); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("SaleId", "ProductId"); + + b.HasIndex("ProductId"); + + b.ToTable("SaleItems"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Product", b => + { + b.HasOne("EcommerceApi.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("EcommerceApi.Models.SaleItem", b => + { + b.HasOne("EcommerceApi.Models.Product", "Product") + .WithMany("SaleItems") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("EcommerceApi.Models.Sale", "Sale") + .WithMany("SaleItems") + .HasForeignKey("SaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Sale"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Category", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Product", b => + { + b.Navigation("SaleItems"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Sale", b => + { + b.Navigation("SaleItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.cs b/EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.cs new file mode 100644 index 00000000..c8e0d252 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.cs @@ -0,0 +1,120 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EcommerceApi.Migrations +{ + /// + public partial class Init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(450)", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Sales", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + SaleDate = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Sales", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Products", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Price = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + CategoryId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + table.ForeignKey( + name: "FK_Products_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "SaleItems", + columns: table => new + { + ProductId = table.Column(type: "int", nullable: false), + SaleId = table.Column(type: "int", nullable: false), + Quantity = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SaleItems", x => new { x.SaleId, x.ProductId }); + table.ForeignKey( + name: "FK_SaleItems_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SaleItems_Sales_SaleId", + column: x => x.SaleId, + principalTable: "Sales", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Categories_Name", + table: "Categories", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Products_CategoryId", + table: "Products", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_SaleItems_ProductId", + table: "SaleItems", + column: "ProductId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SaleItems"); + + migrationBuilder.DropTable( + name: "Products"); + + migrationBuilder.DropTable( + name: "Sales"); + + migrationBuilder.DropTable( + name: "Categories"); + } + } +} diff --git a/EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.Designer.cs b/EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.Designer.cs new file mode 100644 index 00000000..89b18b35 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.Designer.cs @@ -0,0 +1,161 @@ +// +using System; +using EcommerceApi.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EcommerceApi.Migrations +{ + [DbContext(typeof(EcommerceDbContext))] + [Migration("20260212050849_Is_Delete_for_Product")] + partial class Is_Delete_for_Product + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("EcommerceApi.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("SaleDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Sales"); + }); + + modelBuilder.Entity("EcommerceApi.Models.SaleItem", b => + { + b.Property("SaleId") + .HasColumnType("int"); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("SaleId", "ProductId"); + + b.HasIndex("ProductId"); + + b.ToTable("SaleItems"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Product", b => + { + b.HasOne("EcommerceApi.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("EcommerceApi.Models.SaleItem", b => + { + b.HasOne("EcommerceApi.Models.Product", "Product") + .WithMany("SaleItems") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("EcommerceApi.Models.Sale", "Sale") + .WithMany("SaleItems") + .HasForeignKey("SaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Sale"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Category", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Product", b => + { + b.Navigation("SaleItems"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Sale", b => + { + b.Navigation("SaleItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.cs b/EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.cs new file mode 100644 index 00000000..fe669742 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EcommerceApi.Migrations +{ + /// + public partial class Is_Delete_for_Product : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Products", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Products"); + } + } +} diff --git a/EcommerceApi.DiegoPetrola/Migrations/EcommerceDbContextModelSnapshot.cs b/EcommerceApi.DiegoPetrola/Migrations/EcommerceDbContextModelSnapshot.cs new file mode 100644 index 00000000..dd70785d --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Migrations/EcommerceDbContextModelSnapshot.cs @@ -0,0 +1,158 @@ +// +using System; +using EcommerceApi.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EcommerceApi.Migrations +{ + [DbContext(typeof(EcommerceDbContext))] + partial class EcommerceDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("EcommerceApi.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("SaleDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Sales"); + }); + + modelBuilder.Entity("EcommerceApi.Models.SaleItem", b => + { + b.Property("SaleId") + .HasColumnType("int"); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("SaleId", "ProductId"); + + b.HasIndex("ProductId"); + + b.ToTable("SaleItems"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Product", b => + { + b.HasOne("EcommerceApi.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("EcommerceApi.Models.SaleItem", b => + { + b.HasOne("EcommerceApi.Models.Product", "Product") + .WithMany("SaleItems") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("EcommerceApi.Models.Sale", "Sale") + .WithMany("SaleItems") + .HasForeignKey("SaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Sale"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Category", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Product", b => + { + b.Navigation("SaleItems"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Sale", b => + { + b.Navigation("SaleItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EcommerceApi.DiegoPetrola/Models/Product.cs b/EcommerceApi.DiegoPetrola/Models/Product.cs index cff1cd06..7182c53f 100644 --- a/EcommerceApi.DiegoPetrola/Models/Product.cs +++ b/EcommerceApi.DiegoPetrola/Models/Product.cs @@ -6,6 +6,7 @@ public class Product public string Name { get; set; } = string.Empty; public decimal Price { get; set; } public int CategoryId { get; set; } + public bool IsDeleted { get; set; } = false; public Category Category { get; set; } = null!; public ICollection SaleItems { get; set; } = []; } diff --git a/EcommerceApi.DiegoPetrola/Program.cs b/EcommerceApi.DiegoPetrola/Program.cs index c07cfa17..ab427d83 100644 --- a/EcommerceApi.DiegoPetrola/Program.cs +++ b/EcommerceApi.DiegoPetrola/Program.cs @@ -1,18 +1,19 @@ using EcommerceApi.Context; +using EcommerceApi.Services; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); - builder.Services.AddControllers(); builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")) - .UseAsyncSeeding(async (context, _, CancellationToken) => - { - // TODO generate seeding later - //await DatabaseSeeding.CustomSeeding((EcommerceDbContext)context); - }) + // TODO generate seeding later + //.UseAsyncSeeding(async (context, _, CancellationToken) => + //{ + // await DatabaseSeeding.CustomSeeding((EcommerceDbContext)context); + //}) ); - +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddOpenApi(); var app = builder.Build(); @@ -23,7 +24,5 @@ } app.UseHttpsRedirection(); -app.UseAuthorization(); app.MapControllers(); - app.Run(); diff --git a/EcommerceApi.DiegoPetrola/Results/GlobalExceptionHandler.cs b/EcommerceApi.DiegoPetrola/Results/GlobalExceptionHandler.cs new file mode 100644 index 00000000..07de9417 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Results/GlobalExceptionHandler.cs @@ -0,0 +1,40 @@ +namespace EcommerceApi.Results; + +using EcommerceApi.Errors; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc; + +public class GlobalExceptionHandler(ILogger logger) : IExceptionHandler +{ + private readonly ILogger _logger = logger; + + public async ValueTask TryHandleAsync( + HttpContext httpContext, + Exception exception, + CancellationToken cancellationToken) + { + _logger.LogError(exception, "An unhandled exception occurred."); + + var (statusCode, title) = exception switch + { + UnauthorizedAccessException => (StatusCodes.Status401Unauthorized, "Unauthorized"), + ArgumentException => (StatusCodes.Status400BadRequest, "Bad Request"), + NotFoundException => (StatusCodes.Status404NotFound, "Not Found"), + _ => (StatusCodes.Status500InternalServerError, "Internal Server Error") + }; + + var problemDetails = new ProblemDetails + { + Status = statusCode, + Title = title, + Detail = statusCode < 500 ? exception.Message : exception.Message + }; + + httpContext.Response.StatusCode = statusCode; + + await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken); + + return true; + } +} + diff --git a/EcommerceApi.DiegoPetrola/Results/Result.cs b/EcommerceApi.DiegoPetrola/Results/Result.cs new file mode 100644 index 00000000..7785812f --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Results/Result.cs @@ -0,0 +1,42 @@ +using System.Diagnostics.CodeAnalysis; + +namespace EcommerceApi.Results; + +// This Result pattern is largely based on the one from the article: +// www.red-gate.com/simple-talk/development/dotnet-development/the-result-pattern-in-asp-net-core-minimal-apis/ +public class Result +{ + public T? Value { get; } + public ErrorResponse? Error { get; } + [MemberNotNullWhen(true, nameof(Value))] + [MemberNotNullWhen(false, nameof(Error))] + public bool IsSuccess { get; } + private Result(T value) + { + Value = value; + IsSuccess = true; + } + private Result(ErrorResponse error) + { + Error = error; + IsSuccess = false; + } + public static Result Ok(T value) => new(value); + private static Result Fail(ErrorType type, string error) => new(new ErrorResponse(error, type)); + public static Result NotFound(string error) => Fail(ErrorType.NotFound, error); + public static Result Invalid(string error) => Fail(ErrorType.Invalid, error); + public static Result InternalServerError(string error) => Fail(ErrorType.InternalServerError, error); + public static implicit operator Result(T value) => Ok(value); +} + +public record ErrorResponse(string Error, ErrorType ErrorType); + +public enum ErrorType +{ + NotFound, + Invalid, + Unauthorized, + Forbidden, + Conflict, + InternalServerError +} \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs index 289c2214..86d62746 100644 --- a/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs +++ b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs @@ -1,37 +1,56 @@ using EcommerceApi.Context; using EcommerceApi.Models; using EcommerceApi.Models.DTOs; +using EcommerceApi.Results; using Microsoft.EntityFrameworkCore; namespace EcommerceApi.Services; public class CategoriesService(EcommerceDbContext context) { - public async Task> GetCategories() + public async Task>> GetCategories() { var categories = await context.Categories .Where(c => !c.IsDeleted) .Select(c => new CategoryDto(c.Id, c.Name, c.IsDeleted)) .ToListAsync(); - return categories; + + return Result>.Ok(categories); } - public async Task SoftDeleteCategory(int id) + public async Task> GetCategory(int id) { var category = await context.Categories.FindAsync(id); - if (category is null) return; + if (category is null || category.IsDeleted) + return Result.NotFound("Category not found"); + var dto = new CategoryDto(category.Id, category.Name, category.IsDeleted); + return Result.Ok(dto); + } + public async Task> SoftDeleteCategory(int id) + { + var category = await context.Categories.FindAsync(id); + if (category is null) + return Result.NotFound("Record not found."); category.IsDeleted = true; await context.SaveChangesAsync(); - return; + return Result.Ok(null!); } - public async Task CreateCategory(CreateCategoryDto dto) + public async Task> CreateCategory(CreateCategoryDto dto) { var category = new Category { Name = dto.Name }; - context.Categories.Add(category); - await context.SaveChangesAsync(); + try + { + context.Categories.Add(category); + await context.SaveChangesAsync(); + } + catch + { + return Result.Invalid("Duplicated category"); + } - return new CategoryDto(category.Id, category.Name, category.IsDeleted); + var newDto = new CategoryDto(category.Id, category.Name, category.IsDeleted); + return Result.Ok(newDto); } } diff --git a/EcommerceApi.DiegoPetrola/Services/ProductsService.cs b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs new file mode 100644 index 00000000..c8318da3 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs @@ -0,0 +1,68 @@ +using EcommerceApi.Context; +using EcommerceApi.Models; +using EcommerceApi.Models.DTOs; +using EcommerceApi.Results; +using Microsoft.EntityFrameworkCore; + +namespace EcommerceApi.Services; + +public class ProductsService(EcommerceDbContext context) +{ + public async Task>> GetProducts(int page) + { + var products = await context.Products + .Skip(20 * page) + .Take(20) + .Select(p => new ProductDto(p.Id, p.Name, p.Price, p.Category.Id, p.Category.Name)) + .ToListAsync(); + + return Result>.Ok(products); + } + + public async Task> GetProduct(int id) + { + var product = await context.Products + .Include(p => p.Category) + .FirstOrDefaultAsync(p => p.Id == id); + + if (product is null) + return Result.NotFound("Product not found"); + + var dto = new ProductDto(product.Id, product.Name, product.Price, product.Category.Id, product.Category.Name); + return Result.Ok(dto); + } + + public async Task> CreateProduct(CreateProductDto dto) + { + var product = new Product + { + Name = dto.Name, + Price = dto.Price, + CategoryId = dto.CategoryId + }; + + try + { + context.Products.Add(product); + await context.SaveChangesAsync(); + } + catch (DbUpdateException) + { + return Result.Invalid("Invalid product data or duplicate entry"); + } + + var newDto = new ProductDto(product.Id, product.Name, product.Price, product.CategoryId, product.Category.Name); + return Result.Ok(newDto); + } + + public async Task> SoftDeleteProduct(int id) + { + var product = await context.Products.FindAsync(id); + if (product is null) + return Result.NotFound("Product not found"); + + product.IsDeleted = true; + await context.SaveChangesAsync(); + return Result.Ok(null); + } +} From e22acba07880e6bb0d1f32854c060b58fe7ecbe9 Mon Sep 17 00:00:00 2001 From: Diego Leal Date: Thu, 12 Feb 2026 17:29:44 -0300 Subject: [PATCH 05/13] implemented sales service and controller --- .../Controllers/SalesController.cs | 65 ++++++----------- EcommerceApi.DiegoPetrola/EcommerceApi.http | 9 ++- EcommerceApi.DiegoPetrola/Program.cs | 1 + EcommerceApi.DiegoPetrola/Results/Result.cs | 4 +- .../Services/CategoriesService.cs | 6 +- .../Services/ProductsService.cs | 6 +- .../Services/SalesService.cs | 73 +++++++++++++++++++ 7 files changed, 113 insertions(+), 51 deletions(-) create mode 100644 EcommerceApi.DiegoPetrola/Services/SalesService.cs diff --git a/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs b/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs index a0e821be..8e000237 100644 --- a/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs +++ b/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs @@ -1,62 +1,45 @@ -using EcommerceApi.Context; -using EcommerceApi.Models; -using EcommerceApi.Models.DTOs; +using EcommerceApi.Models.DTOs; +using EcommerceApi.Results; +using EcommerceApi.Services; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; namespace EcommerceApi.Controllers; [ApiController] [Route("api/[controller]")] -public class SalesController(EcommerceDbContext context) : ControllerBase +public class SalesController(SalesService service) : ControllerBase { [HttpGet("{id}")] public async Task> GetSale(int id) { - var sale = await context.Sales - .Include(s => s.SaleItems) - .ThenInclude(si => si.Product) - .FirstOrDefaultAsync(s => s.Id == id); - - if (sale is null) return NotFound(); - - var dto = new SaleDto( - sale.Id, - sale.SaleDate, - [.. sale.SaleItems.Select(si => new SaleItemDto( - si.ProductId, - si.Product.Name, - si.Quantity, - si.Product.Price))], - sale.SaleItems.Sum(si => si.Quantity * si.Product.Price) - ); + var res = await service.GetSale(id); + return MapToStatusCode(res); + } - return Ok(dto); + [HttpGet("page/{page}")] + public async Task>> GetSalePage(int page) + { + var res = await service.GetSalesByPage(page); + return MapToStatusCode(res); } [HttpPost] public async Task> CreateSale(CreateSaleDto dto) { - if (dto.Items == null || dto.Items.Count == 0) - return BadRequest("Sale must contain at least one item."); + var res = await service.CreateSale(dto); + return MapToStatusCode(res); + } - var sale = new Sale { SaleDate = DateTime.UtcNow }; + private ActionResult MapToStatusCode(Result res) + { + if (res.IsSuccess) + return Ok(res.Value); - foreach (var item in dto.Items) + return res.Error.ErrorType switch { - var product = await context.Products.FindAsync(item.ProductId); - if (product == null) return BadRequest($"Product {item.ProductId} not found."); - - sale.SaleItems.Add(new SaleItem - { - ProductId = item.ProductId, - Quantity = item.Quantity - }); - } - - context.Sales.Add(sale); - await context.SaveChangesAsync(); - - return CreatedAtAction(nameof(GetSale), new { id = sale.Id }, sale.Id); + ErrorType.NotFound => NotFound(res.Error.Error), + ErrorType.Invalid => BadRequest(res.Error.Error), + _ => Problem(res.Error.Error) + }; } } \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/EcommerceApi.http b/EcommerceApi.DiegoPetrola/EcommerceApi.http index 225b259a..1e0f5bb5 100644 --- a/EcommerceApi.DiegoPetrola/EcommerceApi.http +++ b/EcommerceApi.DiegoPetrola/EcommerceApi.http @@ -13,10 +13,13 @@ Content-Type: application/json { "Name":"Cat" } -### GET PRODUCTS +### GET PRODUCTS PAGE GET {{EcommerceApi_HostAddress}}/api/products/page/1 Accept: application/json -### GET PRODUCTS -GET {{EcommerceApi_HostAddress}}/api/products/1 +### GET SALES PAGE +GET {{EcommerceApi_HostAddress}}/api/sales/page/0 +Accept: application/json +### GET SALES +GET {{EcommerceApi_HostAddress}}/api/sales/0 Accept: application/json ### \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/Program.cs b/EcommerceApi.DiegoPetrola/Program.cs index ab427d83..c02ed50c 100644 --- a/EcommerceApi.DiegoPetrola/Program.cs +++ b/EcommerceApi.DiegoPetrola/Program.cs @@ -14,6 +14,7 @@ ); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddOpenApi(); var app = builder.Build(); diff --git a/EcommerceApi.DiegoPetrola/Results/Result.cs b/EcommerceApi.DiegoPetrola/Results/Result.cs index 7785812f..fba7d274 100644 --- a/EcommerceApi.DiegoPetrola/Results/Result.cs +++ b/EcommerceApi.DiegoPetrola/Results/Result.cs @@ -39,4 +39,6 @@ public enum ErrorType Forbidden, Conflict, InternalServerError -} \ No newline at end of file +} + + diff --git a/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs index 86d62746..5d0202c9 100644 --- a/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs +++ b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs @@ -27,14 +27,14 @@ public async Task> GetCategory(int id) return Result.Ok(dto); } - public async Task> SoftDeleteCategory(int id) + public async Task> SoftDeleteCategory(int id) { var category = await context.Categories.FindAsync(id); if (category is null) - return Result.NotFound("Record not found."); + return Result.NotFound("Record not found."); category.IsDeleted = true; await context.SaveChangesAsync(); - return Result.Ok(null!); + return Result.Ok(null); } public async Task> CreateCategory(CreateCategoryDto dto) diff --git a/EcommerceApi.DiegoPetrola/Services/ProductsService.cs b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs index c8318da3..91a97ff7 100644 --- a/EcommerceApi.DiegoPetrola/Services/ProductsService.cs +++ b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs @@ -55,14 +55,14 @@ public async Task> CreateProduct(CreateProductDto dto) return Result.Ok(newDto); } - public async Task> SoftDeleteProduct(int id) + public async Task> SoftDeleteProduct(int id) { var product = await context.Products.FindAsync(id); if (product is null) - return Result.NotFound("Product not found"); + return Result.NotFound("Product not found"); product.IsDeleted = true; await context.SaveChangesAsync(); - return Result.Ok(null); + return Result.Ok(null); } } diff --git a/EcommerceApi.DiegoPetrola/Services/SalesService.cs b/EcommerceApi.DiegoPetrola/Services/SalesService.cs new file mode 100644 index 00000000..a78935d2 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Services/SalesService.cs @@ -0,0 +1,73 @@ +using EcommerceApi.Context; +using EcommerceApi.Models; +using EcommerceApi.Models.DTOs; +using EcommerceApi.Results; +using Microsoft.EntityFrameworkCore; + +namespace EcommerceApi.Services; + +public class SalesService(EcommerceDbContext context) +{ + private static SaleDto GetSaleDto(Sale sale) + { + return new SaleDto( + sale.Id, + sale.SaleDate, + [.. sale.SaleItems.Select(si => new SaleItemDto( + si.ProductId, + si.Product.Name, + si.Quantity, + si.Product.Price))], + sale.SaleItems.Sum(si => si.Quantity * si.Product.Price) + ); + } + + public async Task>> GetSalesByPage(int page) + { + var sales = await context.Sales + .Skip(page * 20) + .Take(20) + .Select(s => GetSaleDto(s)) + .ToListAsync(); + + return Result>.Ok(sales); + } + + public async Task> GetSale(int id) + { + var sale = await context.Sales + .Include(s => s.SaleItems) + .ThenInclude(si => si.Product) + .FirstOrDefaultAsync(s => s.Id == id); + + if (sale is null) return Result.NotFound("Sale not found"); + + var dto = GetSaleDto(sale); + return Result.Ok(dto); + } + + public async Task> CreateSale(CreateSaleDto dto) + { + if (dto.Items == null || dto.Items.Count == 0) + return Result.Invalid("Sale must contain at least one item."); + + var sale = new Sale { SaleDate = DateTime.UtcNow }; + + foreach (var item in dto.Items) + { + var product = await context.Products.FindAsync(item.ProductId); + if (product == null) return Result.Invalid($"Can't make sale of invalid product {item.ProductId}!"); + + sale.SaleItems.Add(new SaleItem + { + ProductId = item.ProductId, + Quantity = item.Quantity + }); + } + + context.Sales.Add(sale); + await context.SaveChangesAsync(); + + return Result.Ok(GetSaleDto(sale)); + } +} From b3aeada07f981aed990d5c2cf2f56bb12c15fd87 Mon Sep 17 00:00:00 2001 From: Diego Leal Date: Fri, 13 Feb 2026 02:02:20 -0300 Subject: [PATCH 06/13] refactored entities to have ToDto() --- .../Context/EcommerceDbContext.cs | 4 +++ .../Controllers/CategoriesController.cs | 26 +++----------- .../Controllers/ProductsController.cs | 20 +++-------- .../Controllers/ResultExtensions.cs | 36 +++++++++++++++++++ .../Controllers/SalesController.cs | 22 +++--------- EcommerceApi.DiegoPetrola/EcommerceApi.http | 2 +- EcommerceApi.DiegoPetrola/Models/Category.cs | 8 ++++- EcommerceApi.DiegoPetrola/Models/Product.cs | 8 ++++- EcommerceApi.DiegoPetrola/Models/Sale.cs | 13 ++++++- EcommerceApi.DiegoPetrola/Models/SaleItem.cs | 9 ++++- .../Services/CategoriesService.cs | 13 +++---- .../Services/ProductsService.cs | 8 ++--- .../Services/SalesService.cs | 21 ++--------- 13 files changed, 98 insertions(+), 92 deletions(-) create mode 100644 EcommerceApi.DiegoPetrola/Controllers/ResultExtensions.cs diff --git a/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs b/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs index e544364f..771cc1d1 100644 --- a/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs +++ b/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs @@ -42,5 +42,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .HasIndex(c => c.Name) .IsUnique(); + + modelBuilder.Entity().HasQueryFilter(c => !c.IsDeleted); + modelBuilder.Entity().HasQueryFilter(p => !p.IsDeleted); + modelBuilder.Entity().HasQueryFilter(si => !si.Product!.IsDeleted); } } \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs b/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs index b3e483f7..f6d84ccb 100644 --- a/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs +++ b/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs @@ -1,5 +1,4 @@ using EcommerceApi.Models.DTOs; -using EcommerceApi.Results; using EcommerceApi.Services; using Microsoft.AspNetCore.Mvc; @@ -13,44 +12,27 @@ public class CategoriesController(CategoriesService service) : ControllerBase public async Task>> GetCategories() { var res = await service.GetCategories(); - if (!res.IsSuccess) - return Problem(res.Error.Error); - return Ok(res.Value); + return this.ToActionResult(res); } [HttpGet("{id}")] public async Task> GetCategory(int id) { var res = await service.GetCategory(id); - if (!res.IsSuccess) - switch (res.Error.ErrorType) - { - case (ErrorType.NotFound): - return NotFound(res.Error.Error); - default: - return Problem(res.Error.Error); - } - - return Ok(res.Value); + return this.ToActionResult(res); } [HttpPost] public async Task> CreateCategory(CreateCategoryDto dto) { var res = await service.CreateCategory(dto); - if (!res.IsSuccess) - return BadRequest(res.Error.Error); - - return Ok(res.Value); + return this.ToActionResult(res); } [HttpDelete("{id}")] public async Task DeleteCategory(int id) { var res = await service.SoftDeleteCategory(id); - if (!res.IsSuccess) - return Problem(res.Error.Error); - - return NoContent(); + return this.ToActionResult(res); } } diff --git a/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs b/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs index e91f8aa0..4b1aba32 100644 --- a/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs +++ b/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs @@ -1,5 +1,4 @@ using EcommerceApi.Models.DTOs; -using EcommerceApi.Results; using EcommerceApi.Services; using Microsoft.AspNetCore.Mvc; @@ -13,38 +12,27 @@ public class ProductsController(ProductsService service) : ControllerBase public async Task>> GetProducts(int page) { var res = await service.GetProducts(page); - return Ok(res.Value); + return this.ToActionResult(res); } [HttpGet("{id}")] public async Task> GetProductById(int id) { var res = await service.GetProduct(id); - if (!res.IsSuccess) - return res.Error.ErrorType switch - { - (ErrorType.NotFound) => NotFound(res.Error.Error), - _ => Problem(res.Error.Error), - }; - - return Ok(res.Value); + return this.ToActionResult(res); } [HttpPost] public async Task> CreateProduct(CreateProductDto dto) { var res = await service.CreateProduct(dto); - if (!res.IsSuccess) - return BadRequest(res.Error.Error); - return Ok(res.Value); + return this.ToActionResult(res); } [HttpDelete("{id}")] public async Task> DeleteProduct(int id) { var res = await service.SoftDeleteProduct(id); - if (!res.IsSuccess) - return NotFound(res.Error.Error); - return Ok(res.Value); + return this.ToActionResult(res); } } diff --git a/EcommerceApi.DiegoPetrola/Controllers/ResultExtensions.cs b/EcommerceApi.DiegoPetrola/Controllers/ResultExtensions.cs new file mode 100644 index 00000000..e614305a --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Controllers/ResultExtensions.cs @@ -0,0 +1,36 @@ +using EcommerceApi.Models; +using EcommerceApi.Models.DTOs; +using EcommerceApi.Results; +using Microsoft.AspNetCore.Mvc; + +namespace EcommerceApi.Controllers; + +public static class ResultExtensions +{ + public static ActionResult ToActionResult(this ControllerBase controller, Result res) + { + if (res.IsSuccess) + return controller.Ok(res.Value); + + return res.Error.ErrorType switch + { + ErrorType.NotFound => controller.NotFound(res.Error.Error), + ErrorType.Invalid => controller.BadRequest(res.Error.Error), + _ => controller.Problem(res.Error.Error) + }; + } + + public static SaleDto ToSaleDto(Sale sale) + { + return new SaleDto( + sale.Id, + sale.SaleDate, + [.. sale.SaleItems.Select(si => new SaleItemDto( + si.ProductId, + si.Product.Name, + si.Quantity, + si.Product.Price))], + sale.SaleItems.Sum(si => si.Quantity * si.Product.Price) + ); + } +} \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs b/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs index 8e000237..4e893de6 100644 --- a/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs +++ b/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs @@ -1,5 +1,4 @@ using EcommerceApi.Models.DTOs; -using EcommerceApi.Results; using EcommerceApi.Services; using Microsoft.AspNetCore.Mvc; @@ -13,33 +12,20 @@ public class SalesController(SalesService service) : ControllerBase public async Task> GetSale(int id) { var res = await service.GetSale(id); - return MapToStatusCode(res); + return this.ToActionResult(res); } [HttpGet("page/{page}")] public async Task>> GetSalePage(int page) { var res = await service.GetSalesByPage(page); - return MapToStatusCode(res); + return this.ToActionResult(res); } [HttpPost] public async Task> CreateSale(CreateSaleDto dto) { var res = await service.CreateSale(dto); - return MapToStatusCode(res); + return this.ToActionResult(res); } - - private ActionResult MapToStatusCode(Result res) - { - if (res.IsSuccess) - return Ok(res.Value); - - return res.Error.ErrorType switch - { - ErrorType.NotFound => NotFound(res.Error.Error), - ErrorType.Invalid => BadRequest(res.Error.Error), - _ => Problem(res.Error.Error) - }; - } -} \ No newline at end of file +} diff --git a/EcommerceApi.DiegoPetrola/EcommerceApi.http b/EcommerceApi.DiegoPetrola/EcommerceApi.http index 1e0f5bb5..0227b5ea 100644 --- a/EcommerceApi.DiegoPetrola/EcommerceApi.http +++ b/EcommerceApi.DiegoPetrola/EcommerceApi.http @@ -17,7 +17,7 @@ Content-Type: application/json GET {{EcommerceApi_HostAddress}}/api/products/page/1 Accept: application/json ### GET SALES PAGE -GET {{EcommerceApi_HostAddress}}/api/sales/page/0 +GET {{EcommerceApi_HostAddress}}/api/sales/page/1 Accept: application/json ### GET SALES GET {{EcommerceApi_HostAddress}}/api/sales/0 diff --git a/EcommerceApi.DiegoPetrola/Models/Category.cs b/EcommerceApi.DiegoPetrola/Models/Category.cs index cc98184c..d8364eae 100644 --- a/EcommerceApi.DiegoPetrola/Models/Category.cs +++ b/EcommerceApi.DiegoPetrola/Models/Category.cs @@ -1,4 +1,6 @@ -namespace EcommerceApi.Models; +using EcommerceApi.Models.DTOs; + +namespace EcommerceApi.Models; public class Category { @@ -6,4 +8,8 @@ public class Category public string Name { get; set; } = string.Empty; public bool IsDeleted { get; set; } = false; public ICollection Products { get; set; } = []; + public CategoryDto ToDto() + { + return new CategoryDto(Id, Name, IsDeleted); + } } diff --git a/EcommerceApi.DiegoPetrola/Models/Product.cs b/EcommerceApi.DiegoPetrola/Models/Product.cs index 7182c53f..0a8b7428 100644 --- a/EcommerceApi.DiegoPetrola/Models/Product.cs +++ b/EcommerceApi.DiegoPetrola/Models/Product.cs @@ -1,4 +1,6 @@ -namespace EcommerceApi.Models; +using EcommerceApi.Models.DTOs; + +namespace EcommerceApi.Models; public class Product { @@ -9,4 +11,8 @@ public class Product public bool IsDeleted { get; set; } = false; public Category Category { get; set; } = null!; public ICollection SaleItems { get; set; } = []; + public ProductDto ToDto() + { + return new ProductDto(Id, Name, Price, Category.Id, Category.Name); + } } diff --git a/EcommerceApi.DiegoPetrola/Models/Sale.cs b/EcommerceApi.DiegoPetrola/Models/Sale.cs index f7657841..236dc285 100644 --- a/EcommerceApi.DiegoPetrola/Models/Sale.cs +++ b/EcommerceApi.DiegoPetrola/Models/Sale.cs @@ -1,8 +1,19 @@ -namespace EcommerceApi.Models; +using EcommerceApi.Models.DTOs; + +namespace EcommerceApi.Models; public class Sale { public int Id { get; set; } public DateTime SaleDate { get; set; } public ICollection SaleItems { get; set; } = []; + public SaleDto ToDto() + { + return new SaleDto( + Id, + SaleDate, + [.. SaleItems.Select(si => si.ToDto())], + SaleItems.Sum(si => si.Quantity * si.Product.Price) + ); + } } diff --git a/EcommerceApi.DiegoPetrola/Models/SaleItem.cs b/EcommerceApi.DiegoPetrola/Models/SaleItem.cs index 17f385e5..6c9ded23 100644 --- a/EcommerceApi.DiegoPetrola/Models/SaleItem.cs +++ b/EcommerceApi.DiegoPetrola/Models/SaleItem.cs @@ -1,4 +1,6 @@ -namespace EcommerceApi.Models; +using EcommerceApi.Models.DTOs; + +namespace EcommerceApi.Models; public class SaleItem { @@ -7,4 +9,9 @@ public class SaleItem public int SaleId { get; set; } public Sale Sale { get; set; } = null!; public int Quantity { get; set; } + + public SaleItemDto ToDto() + { + return new SaleItemDto(ProductId, Product.Name, Quantity, Product.Price); + } } \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs index 5d0202c9..17a34cc9 100644 --- a/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs +++ b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs @@ -11,8 +11,7 @@ public class CategoriesService(EcommerceDbContext context) public async Task>> GetCategories() { var categories = await context.Categories - .Where(c => !c.IsDeleted) - .Select(c => new CategoryDto(c.Id, c.Name, c.IsDeleted)) + .Select(c => c.ToDto()) .ToListAsync(); return Result>.Ok(categories); @@ -21,17 +20,16 @@ public async Task>> GetCategories() public async Task> GetCategory(int id) { var category = await context.Categories.FindAsync(id); - if (category is null || category.IsDeleted) + if (category is null) return Result.NotFound("Category not found"); - var dto = new CategoryDto(category.Id, category.Name, category.IsDeleted); - return Result.Ok(dto); + return Result.Ok(category.ToDto()); } public async Task> SoftDeleteCategory(int id) { var category = await context.Categories.FindAsync(id); if (category is null) - return Result.NotFound("Record not found."); + return Result.NotFound("Category not found."); category.IsDeleted = true; await context.SaveChangesAsync(); return Result.Ok(null); @@ -50,7 +48,6 @@ public async Task> CreateCategory(CreateCategoryDto dto) return Result.Invalid("Duplicated category"); } - var newDto = new CategoryDto(category.Id, category.Name, category.IsDeleted); - return Result.Ok(newDto); + return Result.Ok(category.ToDto()); } } diff --git a/EcommerceApi.DiegoPetrola/Services/ProductsService.cs b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs index 91a97ff7..5ed00057 100644 --- a/EcommerceApi.DiegoPetrola/Services/ProductsService.cs +++ b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs @@ -13,7 +13,7 @@ public async Task>> GetProducts(int page) var products = await context.Products .Skip(20 * page) .Take(20) - .Select(p => new ProductDto(p.Id, p.Name, p.Price, p.Category.Id, p.Category.Name)) + .Select(p => p.ToDto()) .ToListAsync(); return Result>.Ok(products); @@ -28,8 +28,7 @@ public async Task> GetProduct(int id) if (product is null) return Result.NotFound("Product not found"); - var dto = new ProductDto(product.Id, product.Name, product.Price, product.Category.Id, product.Category.Name); - return Result.Ok(dto); + return Result.Ok(product.ToDto()); } public async Task> CreateProduct(CreateProductDto dto) @@ -51,8 +50,7 @@ public async Task> CreateProduct(CreateProductDto dto) return Result.Invalid("Invalid product data or duplicate entry"); } - var newDto = new ProductDto(product.Id, product.Name, product.Price, product.CategoryId, product.Category.Name); - return Result.Ok(newDto); + return Result.Ok(product.ToDto()); } public async Task> SoftDeleteProduct(int id) diff --git a/EcommerceApi.DiegoPetrola/Services/SalesService.cs b/EcommerceApi.DiegoPetrola/Services/SalesService.cs index a78935d2..365aba78 100644 --- a/EcommerceApi.DiegoPetrola/Services/SalesService.cs +++ b/EcommerceApi.DiegoPetrola/Services/SalesService.cs @@ -8,26 +8,12 @@ namespace EcommerceApi.Services; public class SalesService(EcommerceDbContext context) { - private static SaleDto GetSaleDto(Sale sale) - { - return new SaleDto( - sale.Id, - sale.SaleDate, - [.. sale.SaleItems.Select(si => new SaleItemDto( - si.ProductId, - si.Product.Name, - si.Quantity, - si.Product.Price))], - sale.SaleItems.Sum(si => si.Quantity * si.Product.Price) - ); - } - public async Task>> GetSalesByPage(int page) { var sales = await context.Sales .Skip(page * 20) .Take(20) - .Select(s => GetSaleDto(s)) + .Select(s => s.ToDto()) .ToListAsync(); return Result>.Ok(sales); @@ -42,8 +28,7 @@ public async Task> GetSale(int id) if (sale is null) return Result.NotFound("Sale not found"); - var dto = GetSaleDto(sale); - return Result.Ok(dto); + return Result.Ok(sale.ToDto()); } public async Task> CreateSale(CreateSaleDto dto) @@ -68,6 +53,6 @@ public async Task> CreateSale(CreateSaleDto dto) context.Sales.Add(sale); await context.SaveChangesAsync(); - return Result.Ok(GetSaleDto(sale)); + return Result.Ok(sale.ToDto()); } } From 15d6d00d7e46a85f0a71fcae096f5c0988890c96 Mon Sep 17 00:00:00 2001 From: Diego Leal Date: Sun, 15 Feb 2026 18:15:32 -0300 Subject: [PATCH 07/13] added Sale IsDeleted property --- .../Context/EcommerceDbContext.cs | 10 +- .../Controllers/CategoriesController.cs | 9 + .../Controllers/SalesController.cs | 7 + EcommerceApi.DiegoPetrola/EcommerceApi.csproj | 4 + ...20260215210358_Sale_Is_Deleted.Designer.cs | 171 ++++++++++++++++++ .../20260215210358_Sale_Is_Deleted.cs | 53 ++++++ .../EcommerceDbContextModelSnapshot.cs | 14 +- .../Models/DTOs/CommerceDtos.cs | 4 +- EcommerceApi.DiegoPetrola/Models/Sale.cs | 3 +- EcommerceApi.DiegoPetrola/Models/SaleItem.cs | 10 +- .../Properties/launchSettings.json | 23 ++- .../Services/CategoriesService.cs | 17 ++ .../Services/ProductsService.cs | 4 + .../Services/SalesService.cs | 24 ++- 14 files changed, 329 insertions(+), 24 deletions(-) create mode 100644 EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.Designer.cs create mode 100644 EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.cs diff --git a/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs b/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs index 771cc1d1..fc3a8a09 100644 --- a/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs +++ b/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs @@ -36,15 +36,23 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .Property(p => p.Price) .HasPrecision(18, 2); + modelBuilder.Entity() + .Property(p => p.ProductPrice) + .HasPrecision(18, 2); + modelBuilder.Entity() .HasKey(si => new { si.SaleId, si.ProductId }); + modelBuilder.Entity() + .HasOne(si => si.Product) + .WithMany(p => p.SaleItems) + .IsRequired(false); + modelBuilder.Entity() .HasIndex(c => c.Name) .IsUnique(); modelBuilder.Entity().HasQueryFilter(c => !c.IsDeleted); modelBuilder.Entity().HasQueryFilter(p => !p.IsDeleted); - modelBuilder.Entity().HasQueryFilter(si => !si.Product!.IsDeleted); } } \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs b/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs index f6d84ccb..d75d082a 100644 --- a/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs +++ b/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs @@ -29,6 +29,15 @@ public async Task> CreateCategory(CreateCategoryDto dt return this.ToActionResult(res); } + [HttpPut("{id}")] + public async Task DeleteCategory(int id, CategoryDto dto) + { + if (dto.Id != id) + return BadRequest(); + var res = await service.UpdateCategory(dto); + return this.ToActionResult(res); + } + [HttpDelete("{id}")] public async Task DeleteCategory(int id) { diff --git a/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs b/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs index 4e893de6..65786d20 100644 --- a/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs +++ b/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs @@ -28,4 +28,11 @@ public async Task> CreateSale(CreateSaleDto dto) var res = await service.CreateSale(dto); return this.ToActionResult(res); } + + [HttpDelete("{id}")] + public async Task> DeleteSale(int id) + { + var res = await service.DeleteSale(id); + return this.ToActionResult(res); + } } diff --git a/EcommerceApi.DiegoPetrola/EcommerceApi.csproj b/EcommerceApi.DiegoPetrola/EcommerceApi.csproj index 570457c6..3b3604c8 100644 --- a/EcommerceApi.DiegoPetrola/EcommerceApi.csproj +++ b/EcommerceApi.DiegoPetrola/EcommerceApi.csproj @@ -16,4 +16,8 @@ + + + + diff --git a/EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.Designer.cs b/EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.Designer.cs new file mode 100644 index 00000000..4afe852f --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.Designer.cs @@ -0,0 +1,171 @@ +// +using System; +using EcommerceApi.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EcommerceApi.Migrations +{ + [DbContext(typeof(EcommerceDbContext))] + [Migration("20260215210358_Sale_Is_Deleted")] + partial class Sale_Is_Deleted + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("EcommerceApi.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("SaleDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Sales"); + }); + + modelBuilder.Entity("EcommerceApi.Models.SaleItem", b => + { + b.Property("SaleId") + .HasColumnType("int"); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProductPrice") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("SaleId", "ProductId"); + + b.HasIndex("ProductId"); + + b.ToTable("SaleItems"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Product", b => + { + b.HasOne("EcommerceApi.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("EcommerceApi.Models.SaleItem", b => + { + b.HasOne("EcommerceApi.Models.Product", "Product") + .WithMany("SaleItems") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("EcommerceApi.Models.Sale", "Sale") + .WithMany("SaleItems") + .HasForeignKey("SaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Sale"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Category", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Product", b => + { + b.Navigation("SaleItems"); + }); + + modelBuilder.Entity("EcommerceApi.Models.Sale", b => + { + b.Navigation("SaleItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.cs b/EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.cs new file mode 100644 index 00000000..14918df6 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.cs @@ -0,0 +1,53 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EcommerceApi.Migrations +{ + /// + public partial class Sale_Is_Deleted : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Sales", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "ProductName", + table: "SaleItems", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "ProductPrice", + table: "SaleItems", + type: "decimal(18,2)", + precision: 18, + scale: 2, + nullable: false, + defaultValue: 0m); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Sales"); + + migrationBuilder.DropColumn( + name: "ProductName", + table: "SaleItems"); + + migrationBuilder.DropColumn( + name: "ProductPrice", + table: "SaleItems"); + } + } +} diff --git a/EcommerceApi.DiegoPetrola/Migrations/EcommerceDbContextModelSnapshot.cs b/EcommerceApi.DiegoPetrola/Migrations/EcommerceDbContextModelSnapshot.cs index dd70785d..dc6ee51b 100644 --- a/EcommerceApi.DiegoPetrola/Migrations/EcommerceDbContextModelSnapshot.cs +++ b/EcommerceApi.DiegoPetrola/Migrations/EcommerceDbContextModelSnapshot.cs @@ -82,6 +82,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("IsDeleted") + .HasColumnType("bit"); + b.Property("SaleDate") .HasColumnType("datetime2"); @@ -98,6 +101,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ProductId") .HasColumnType("int"); + b.Property("ProductName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProductPrice") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + b.Property("Quantity") .HasColumnType("int"); @@ -124,8 +135,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("EcommerceApi.Models.Product", "Product") .WithMany("SaleItems") .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); + .OnDelete(DeleteBehavior.Restrict); b.HasOne("EcommerceApi.Models.Sale", "Sale") .WithMany("SaleItems") diff --git a/EcommerceApi.DiegoPetrola/Models/DTOs/CommerceDtos.cs b/EcommerceApi.DiegoPetrola/Models/DTOs/CommerceDtos.cs index cd90e0f3..53f56560 100644 --- a/EcommerceApi.DiegoPetrola/Models/DTOs/CommerceDtos.cs +++ b/EcommerceApi.DiegoPetrola/Models/DTOs/CommerceDtos.cs @@ -4,7 +4,7 @@ public record CategoryDto(int Id, string Name, bool IsDeleted); public record CreateCategoryDto(string Name); public record ProductDto(int Id, string Name, decimal Price, int CategoryId, string CategoryName); public record CreateProductDto(string Name, decimal Price, int CategoryId); -public record SaleItemDto(int ProductId, string ProductName, int Quantity, decimal UnitPrice); -public record SaleDto(int Id, DateTime SaleDate, List Items, decimal TotalAmount); +public record SaleItemDto(int? ProductId, string ProductName, int Quantity, decimal UnitPrice); +public record SaleDto(int Id, DateTime SaleDate, List SaleItems, decimal TotalAmount); public record CreateSaleDto(List Items); public record CreateSaleItemDto(int ProductId, int Quantity); diff --git a/EcommerceApi.DiegoPetrola/Models/Sale.cs b/EcommerceApi.DiegoPetrola/Models/Sale.cs index 236dc285..42ccd77b 100644 --- a/EcommerceApi.DiegoPetrola/Models/Sale.cs +++ b/EcommerceApi.DiegoPetrola/Models/Sale.cs @@ -6,6 +6,7 @@ public class Sale { public int Id { get; set; } public DateTime SaleDate { get; set; } + public bool IsDeleted { get; set; } = false; public ICollection SaleItems { get; set; } = []; public SaleDto ToDto() { @@ -13,7 +14,7 @@ public SaleDto ToDto() Id, SaleDate, [.. SaleItems.Select(si => si.ToDto())], - SaleItems.Sum(si => si.Quantity * si.Product.Price) + SaleItems.Sum(si => si.Quantity * si.ProductPrice) ); } } diff --git a/EcommerceApi.DiegoPetrola/Models/SaleItem.cs b/EcommerceApi.DiegoPetrola/Models/SaleItem.cs index 6c9ded23..ab6e47c1 100644 --- a/EcommerceApi.DiegoPetrola/Models/SaleItem.cs +++ b/EcommerceApi.DiegoPetrola/Models/SaleItem.cs @@ -4,14 +4,16 @@ namespace EcommerceApi.Models; public class SaleItem { - public int ProductId { get; set; } - public Product Product { get; set; } = null!; + public int? ProductId { get; set; } + public Product? Product { get; set; } = null!; + public string ProductName { get; set; } = string.Empty; + public decimal ProductPrice { get; set; } public int SaleId { get; set; } public Sale Sale { get; set; } = null!; public int Quantity { get; set; } public SaleItemDto ToDto() { - return new SaleItemDto(ProductId, Product.Name, Quantity, Product.Price); + return new SaleItemDto(ProductId, ProductName, Quantity, ProductPrice); } -} \ No newline at end of file +} diff --git a/EcommerceApi.DiegoPetrola/Properties/launchSettings.json b/EcommerceApi.DiegoPetrola/Properties/launchSettings.json index 8d908ba7..7c7d2afa 100644 --- a/EcommerceApi.DiegoPetrola/Properties/launchSettings.json +++ b/EcommerceApi.DiegoPetrola/Properties/launchSettings.json @@ -1,23 +1,22 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", +{ "profiles": { "http": { "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "http://localhost:5112", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5112" }, "https": { "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "https://localhost:7216;http://localhost:5112", + "workingDirectory": "./", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7216;http://localhost:5112" } - } -} + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs index 17a34cc9..1ae11ac4 100644 --- a/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs +++ b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs @@ -50,4 +50,21 @@ public async Task> CreateCategory(CreateCategoryDto dto) return Result.Ok(category.ToDto()); } + + public async Task> UpdateCategory(CategoryDto dto) + { + var category = await context.Categories.FindAsync(dto.Id); + if (category is null) + return Result.NotFound("Category not found"); + category.Name = dto.Name; + try + { + await context.SaveChangesAsync(); + return Result.Ok(category.ToDto()); + } + catch + { + return Result.Invalid("Duplicated category"); + } + } } diff --git a/EcommerceApi.DiegoPetrola/Services/ProductsService.cs b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs index 5ed00057..71094b9c 100644 --- a/EcommerceApi.DiegoPetrola/Services/ProductsService.cs +++ b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs @@ -11,6 +11,7 @@ public class ProductsService(EcommerceDbContext context) public async Task>> GetProducts(int page) { var products = await context.Products + .Include(p => p.Category) .Skip(20 * page) .Take(20) .Select(p => p.ToDto()) @@ -44,6 +45,9 @@ public async Task> CreateProduct(CreateProductDto dto) { context.Products.Add(product); await context.SaveChangesAsync(); + await context.Entry(product) + .Reference(p => p.Category) + .LoadAsync(); } catch (DbUpdateException) { diff --git a/EcommerceApi.DiegoPetrola/Services/SalesService.cs b/EcommerceApi.DiegoPetrola/Services/SalesService.cs index 365aba78..a46a866a 100644 --- a/EcommerceApi.DiegoPetrola/Services/SalesService.cs +++ b/EcommerceApi.DiegoPetrola/Services/SalesService.cs @@ -11,6 +11,7 @@ public class SalesService(EcommerceDbContext context) public async Task>> GetSalesByPage(int page) { var sales = await context.Sales + .IgnoreQueryFilters() .Skip(page * 20) .Take(20) .Select(s => s.ToDto()) @@ -22,6 +23,7 @@ public async Task>> GetSalesByPage(int page) public async Task> GetSale(int id) { var sale = await context.Sales + .IgnoreQueryFilters() .Include(s => s.SaleItems) .ThenInclude(si => si.Product) .FirstOrDefaultAsync(s => s.Id == id); @@ -46,13 +48,31 @@ public async Task> CreateSale(CreateSaleDto dto) sale.SaleItems.Add(new SaleItem { ProductId = item.ProductId, - Quantity = item.Quantity + Quantity = item.Quantity, + ProductName = product.Name, + ProductPrice = product.Price }); } context.Sales.Add(sale); await context.SaveChangesAsync(); - return Result.Ok(sale.ToDto()); } + + public async Task> DeleteSale(int id) + { + var sale = await context.Sales.FindAsync(id); + if (sale is null) + return Result.NotFound("Sale not found"); + try + { + sale.IsDeleted = true; + await context.SaveChangesAsync(); + return Result.Ok(sale.ToDto()); + } + catch + { + return Result.InternalServerError("Something went wrong"); + } + } } From 22e1f50c6df21d3c2ed6aa53d44309d10ae69526 Mon Sep 17 00:00:00 2001 From: Diego Leal Date: Mon, 16 Feb 2026 01:36:50 -0300 Subject: [PATCH 08/13] Finished testing --- .../Context/EcommerceDbContext.cs | 3 +- .../Controllers/ProductsController.cs | 9 + .../Dotnet Ecommerce.postman_collection.json | 678 ++++++++++++++++++ EcommerceApi.DiegoPetrola/EcommerceApi.csproj | 4 - .../20260209234942_Init.Designer.cs | 158 ---- ...12050849_Is_Delete_for_Product.Designer.cs | 161 ----- .../20260212050849_Is_Delete_for_Product.cs | 29 - .../20260215210358_Sale_Is_Deleted.cs | 53 -- ....cs => 20260216042746_Initial.Designer.cs} | 7 +- ...4942_Init.cs => 20260216042746_Initial.cs} | 13 +- .../EcommerceDbContextModelSnapshot.cs | 3 - EcommerceApi.DiegoPetrola/Models/Category.cs | 3 +- .../Models/DTOs/CommerceDtos.cs | 2 +- .../Results/GlobalExceptionHandler.cs | 40 -- .../Services/CategoriesService.cs | 13 +- .../Services/ProductsService.cs | 36 +- .../Services/SalesService.cs | 17 +- 17 files changed, 754 insertions(+), 475 deletions(-) create mode 100644 EcommerceApi.DiegoPetrola/Dotnet Ecommerce.postman_collection.json delete mode 100644 EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.Designer.cs delete mode 100644 EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.Designer.cs delete mode 100644 EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.cs delete mode 100644 EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.cs rename EcommerceApi.DiegoPetrola/Migrations/{20260215210358_Sale_Is_Deleted.Designer.cs => 20260216042746_Initial.Designer.cs} (96%) rename EcommerceApi.DiegoPetrola/Migrations/{20260209234942_Init.cs => 20260216042746_Initial.cs} (91%) delete mode 100644 EcommerceApi.DiegoPetrola/Results/GlobalExceptionHandler.cs diff --git a/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs b/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs index fc3a8a09..85c6083c 100644 --- a/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs +++ b/EcommerceApi.DiegoPetrola/Context/EcommerceDbContext.cs @@ -52,7 +52,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasIndex(c => c.Name) .IsUnique(); - modelBuilder.Entity().HasQueryFilter(c => !c.IsDeleted); modelBuilder.Entity().HasQueryFilter(p => !p.IsDeleted); + modelBuilder.Entity().HasQueryFilter(s => !s.IsDeleted); + modelBuilder.Entity().HasQueryFilter(si => !si.Sale.IsDeleted); } } \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs b/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs index 4b1aba32..de6dedaa 100644 --- a/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs +++ b/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs @@ -35,4 +35,13 @@ public async Task> DeleteProduct(int id) var res = await service.SoftDeleteProduct(id); return this.ToActionResult(res); } + + [HttpPut("{id}")] + public async Task> UpdateProduct(int id, ProductDto dto) + { + if (id != dto.Id) + return BadRequest(); + var res = await service.UpdateProduct(dto); + return this.ToActionResult(res); + } } diff --git a/EcommerceApi.DiegoPetrola/Dotnet Ecommerce.postman_collection.json b/EcommerceApi.DiegoPetrola/Dotnet Ecommerce.postman_collection.json new file mode 100644 index 00000000..1c8302d0 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Dotnet Ecommerce.postman_collection.json @@ -0,0 +1,678 @@ +{ + "info": { + "_postman_id": "c3c76d52-7143-4d8c-8448-9b97ab2fc5e8", + "name": "Dotnet Ecommerce", + "description": "# 🚀 Get started here\n\nThis template guides you through CRUD operations (GET, POST, PUT, DELETE), variables, and tests.\n\n## 🔖 **How to use this template**\n\n#### **Step 1: Send requests**\n\nRESTful APIs allow you to perform CRUD operations using the POST, GET, PUT, and DELETE HTTP methods.\n\nThis collection contains each of these [request](https://learning.postman.com/docs/sending-requests/requests/) types. Open each request and click \"Send\" to see what happens.\n\n#### **Step 2: View responses**\n\nObserve the response tab for status code (200 OK), response time, and size.\n\n#### **Step 3: Send new Body data**\n\nUpdate or add new data in \"Body\" in the POST request. Typically, Body data is also used in PUT request.\n\n```\n{\n \"name\": \"Add your name in the body\"\n}\n\n ```\n\n#### **Step 4: Update the variable**\n\nVariables enable you to store and reuse values in Postman. We have created a [variable](https://learning.postman.com/docs/sending-requests/variables/) called `base_url` with the sample request [https://postman-api-learner.glitch.me](https://postman-api-learner.glitch.me). Replace it with your API endpoint to customize this collection.\n\n#### **Step 5: Add tests in the \"Scripts\" tab**\n\nAdding tests to your requests can help you confirm that your API is working as expected. You can write test scripts in JavaScript and view the output in the \"Test Results\" tab.\n\n\"\"\n\n## 💪 Pro tips\n\n- Use folders to group related requests and organize the collection.\n \n- Add more [scripts](https://learning.postman.com/docs/writing-scripts/intro-to-scripts/) to verify if the API works as expected and execute workflows.\n \n\n## 💡Related templates\n\n[API testing basics](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=e9a37a28-055b-49cd-8c7e-97494a21eb54&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719) \n[API documentation](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=e9c28f47-1253-44af-a2f3-20dce4da1f18&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719) \n[Authorization methods](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=31a9a6ed-4cdf-4ced-984c-d12c9aec1c27&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "28759469" + }, + "item": [ + { + "name": "Categories", + "item": [ + { + "name": "Get category", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "packages": {}, + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/categories", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "categories" + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Post data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Successful POST request\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200, 201]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"Name\": \"TestCat123\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/categories", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "categories" + ] + }, + "description": "This is a POST request, submitting data to an API via the request body. This request submits JSON data, and the data is reflected in the response.\n\nA successful POST request typically returns a `200 OK` or `201 Created` response code." + }, + "response": [] + }, + { + "name": "Update data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Successful PUT request\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n\t\"name\": \"Category 5\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/categories/1", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "categories", + "1" + ] + }, + "description": "This is a PUT request and it is used to overwrite an existing piece of data. For instance, after you create an entity with a POST request, you may want to modify that later. You can do that using a PUT request. You typically identify the entity being updated by including an identifier in the URL (eg. `id=1`).\n\nA successful PUT request typically returns a `200 OK`, `201 Created`, or `204 No Content` response code." + }, + "response": [] + }, + { + "name": "Delete data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Successful DELETE request\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200, 202, 204]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/categories/1", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "categories", + "1" + ] + }, + "description": "This is a DELETE request, and it is used to delete data that was previously created via a POST request. You typically identify the entity being updated by including an identifier in the URL (eg. `id=1`).\n\nA successful DELETE request typically returns a `200 OK`, `202 Accepted`, or `204 No Content` response code." + }, + "response": [] + }, + { + "name": "Post data Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Successful POST request\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200, 201]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"Name\": \"TestCat1\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/categories", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "categories" + ] + }, + "description": "This is a POST request, submitting data to an API via the request body. This request submits JSON data, and the data is reflected in the response.\n\nA successful POST request typically returns a `200 OK` or `201 Created` response code." + }, + "response": [] + } + ] + }, + { + "name": "Products", + "item": [ + { + "name": "Get Prod Page", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/products/page/0", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "products", + "page", + "0" + ] + } + }, + "response": [] + }, + { + "name": "New Product", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Computer2\",\r\n \"Price\": 150,\r\n \"CategoryId\": 2\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/products", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "products" + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "New Product Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Computer2\",\r\n \"Price\": 150,\r\n \"CategoryId\": 2\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/products", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "products" + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "New Product Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Name\": \"Computer3\",\r\n \"Price\": 150,\r\n \"CategoryId\": 2\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/products", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "products" + ] + }, + "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." + }, + "response": [] + }, + { + "name": "Get Prod", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/products/2", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "products", + "2" + ] + } + }, + "response": [] + }, + { + "name": "Update Prod", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Successful PUT request\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"Name\": \"Computer5\",\n \"Price\": 150,\n \"CategoryId\": 2,\n \"CategoryName\": \"Ca1\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/products/1", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "products", + "1" + ] + }, + "description": "This is a PUT request and it is used to overwrite an existing piece of data. For instance, after you create an entity with a POST request, you may want to modify that later. You can do that using a PUT request. You typically identify the entity being updated by including an identifier in the URL (eg. `id=1`).\n\nA successful PUT request typically returns a `200 OK`, `201 Created`, or `204 No Content` response code." + }, + "response": [] + }, + { + "name": "Del Prod", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Successful PUT request\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]);", + "});", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/products/1", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "products", + "1" + ] + }, + "description": "This is a PUT request and it is used to overwrite an existing piece of data. For instance, after you create an entity with a POST request, you may want to modify that later. You can do that using a PUT request. You typically identify the entity being updated by including an identifier in the URL (eg. `id=1`).\n\nA successful PUT request typically returns a `200 OK`, `201 Created`, or `204 No Content` response code." + }, + "response": [] + } + ] + }, + { + "name": "Sale", + "item": [ + { + "name": "New Sale", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"Items\":[\r\n {\r\n \"productId\": 2,\r\n \"quantity\": 2\r\n }\r\n ]\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/sales", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "sales" + ] + } + }, + "response": [] + }, + { + "name": "Get Sale", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base_url}}/api/sales/1", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "sales", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Sale Page", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base_url}}/api/sales/page/0", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "sales", + "page", + "0" + ] + } + }, + "response": [] + }, + { + "name": "Delete Sale", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base_url}}/api/sales/1", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "sales", + "1" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "requests": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "requests": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "id", + "value": "1" + }, + { + "key": "base_url", + "value": "https://template.postman-echo.com" + } + ] +} \ No newline at end of file diff --git a/EcommerceApi.DiegoPetrola/EcommerceApi.csproj b/EcommerceApi.DiegoPetrola/EcommerceApi.csproj index 3b3604c8..570457c6 100644 --- a/EcommerceApi.DiegoPetrola/EcommerceApi.csproj +++ b/EcommerceApi.DiegoPetrola/EcommerceApi.csproj @@ -16,8 +16,4 @@ - - - - diff --git a/EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.Designer.cs b/EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.Designer.cs deleted file mode 100644 index e3c65848..00000000 --- a/EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.Designer.cs +++ /dev/null @@ -1,158 +0,0 @@ -// -using System; -using EcommerceApi.Context; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace EcommerceApi.Migrations -{ - [DbContext(typeof(EcommerceDbContext))] - [Migration("20260209234942_Init")] - partial class Init - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.2") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("EcommerceApi.Models.Category", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - - b.Property("IsDeleted") - .HasColumnType("bit"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("Categories"); - }); - - modelBuilder.Entity("EcommerceApi.Models.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - - b.Property("CategoryId") - .HasColumnType("int"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("decimal(18,2)"); - - b.HasKey("Id"); - - b.HasIndex("CategoryId"); - - b.ToTable("Products"); - }); - - modelBuilder.Entity("EcommerceApi.Models.Sale", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - - b.Property("SaleDate") - .HasColumnType("datetime2"); - - b.HasKey("Id"); - - b.ToTable("Sales"); - }); - - modelBuilder.Entity("EcommerceApi.Models.SaleItem", b => - { - b.Property("SaleId") - .HasColumnType("int"); - - b.Property("ProductId") - .HasColumnType("int"); - - b.Property("Quantity") - .HasColumnType("int"); - - b.HasKey("SaleId", "ProductId"); - - b.HasIndex("ProductId"); - - b.ToTable("SaleItems"); - }); - - modelBuilder.Entity("EcommerceApi.Models.Product", b => - { - b.HasOne("EcommerceApi.Models.Category", "Category") - .WithMany("Products") - .HasForeignKey("CategoryId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Category"); - }); - - modelBuilder.Entity("EcommerceApi.Models.SaleItem", b => - { - b.HasOne("EcommerceApi.Models.Product", "Product") - .WithMany("SaleItems") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("EcommerceApi.Models.Sale", "Sale") - .WithMany("SaleItems") - .HasForeignKey("SaleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Product"); - - b.Navigation("Sale"); - }); - - modelBuilder.Entity("EcommerceApi.Models.Category", b => - { - b.Navigation("Products"); - }); - - modelBuilder.Entity("EcommerceApi.Models.Product", b => - { - b.Navigation("SaleItems"); - }); - - modelBuilder.Entity("EcommerceApi.Models.Sale", b => - { - b.Navigation("SaleItems"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.Designer.cs b/EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.Designer.cs deleted file mode 100644 index 89b18b35..00000000 --- a/EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.Designer.cs +++ /dev/null @@ -1,161 +0,0 @@ -// -using System; -using EcommerceApi.Context; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace EcommerceApi.Migrations -{ - [DbContext(typeof(EcommerceDbContext))] - [Migration("20260212050849_Is_Delete_for_Product")] - partial class Is_Delete_for_Product - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.3") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("EcommerceApi.Models.Category", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - - b.Property("IsDeleted") - .HasColumnType("bit"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("Categories"); - }); - - modelBuilder.Entity("EcommerceApi.Models.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - - b.Property("CategoryId") - .HasColumnType("int"); - - b.Property("IsDeleted") - .HasColumnType("bit"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("decimal(18,2)"); - - b.HasKey("Id"); - - b.HasIndex("CategoryId"); - - b.ToTable("Products"); - }); - - modelBuilder.Entity("EcommerceApi.Models.Sale", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - - b.Property("SaleDate") - .HasColumnType("datetime2"); - - b.HasKey("Id"); - - b.ToTable("Sales"); - }); - - modelBuilder.Entity("EcommerceApi.Models.SaleItem", b => - { - b.Property("SaleId") - .HasColumnType("int"); - - b.Property("ProductId") - .HasColumnType("int"); - - b.Property("Quantity") - .HasColumnType("int"); - - b.HasKey("SaleId", "ProductId"); - - b.HasIndex("ProductId"); - - b.ToTable("SaleItems"); - }); - - modelBuilder.Entity("EcommerceApi.Models.Product", b => - { - b.HasOne("EcommerceApi.Models.Category", "Category") - .WithMany("Products") - .HasForeignKey("CategoryId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Category"); - }); - - modelBuilder.Entity("EcommerceApi.Models.SaleItem", b => - { - b.HasOne("EcommerceApi.Models.Product", "Product") - .WithMany("SaleItems") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("EcommerceApi.Models.Sale", "Sale") - .WithMany("SaleItems") - .HasForeignKey("SaleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Product"); - - b.Navigation("Sale"); - }); - - modelBuilder.Entity("EcommerceApi.Models.Category", b => - { - b.Navigation("Products"); - }); - - modelBuilder.Entity("EcommerceApi.Models.Product", b => - { - b.Navigation("SaleItems"); - }); - - modelBuilder.Entity("EcommerceApi.Models.Sale", b => - { - b.Navigation("SaleItems"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.cs b/EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.cs deleted file mode 100644 index fe669742..00000000 --- a/EcommerceApi.DiegoPetrola/Migrations/20260212050849_Is_Delete_for_Product.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace EcommerceApi.Migrations -{ - /// - public partial class Is_Delete_for_Product : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsDeleted", - table: "Products", - type: "bit", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "IsDeleted", - table: "Products"); - } - } -} diff --git a/EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.cs b/EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.cs deleted file mode 100644 index 14918df6..00000000 --- a/EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace EcommerceApi.Migrations -{ - /// - public partial class Sale_Is_Deleted : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsDeleted", - table: "Sales", - type: "bit", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "ProductName", - table: "SaleItems", - type: "nvarchar(max)", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "ProductPrice", - table: "SaleItems", - type: "decimal(18,2)", - precision: 18, - scale: 2, - nullable: false, - defaultValue: 0m); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "IsDeleted", - table: "Sales"); - - migrationBuilder.DropColumn( - name: "ProductName", - table: "SaleItems"); - - migrationBuilder.DropColumn( - name: "ProductPrice", - table: "SaleItems"); - } - } -} diff --git a/EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.Designer.cs b/EcommerceApi.DiegoPetrola/Migrations/20260216042746_Initial.Designer.cs similarity index 96% rename from EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.Designer.cs rename to EcommerceApi.DiegoPetrola/Migrations/20260216042746_Initial.Designer.cs index 4afe852f..443691ad 100644 --- a/EcommerceApi.DiegoPetrola/Migrations/20260215210358_Sale_Is_Deleted.Designer.cs +++ b/EcommerceApi.DiegoPetrola/Migrations/20260216042746_Initial.Designer.cs @@ -12,8 +12,8 @@ namespace EcommerceApi.Migrations { [DbContext(typeof(EcommerceDbContext))] - [Migration("20260215210358_Sale_Is_Deleted")] - partial class Sale_Is_Deleted + [Migration("20260216042746_Initial")] + partial class Initial { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -33,9 +33,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("IsDeleted") - .HasColumnType("bit"); - b.Property("Name") .IsRequired() .HasColumnType("nvarchar(450)"); diff --git a/EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.cs b/EcommerceApi.DiegoPetrola/Migrations/20260216042746_Initial.cs similarity index 91% rename from EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.cs rename to EcommerceApi.DiegoPetrola/Migrations/20260216042746_Initial.cs index c8e0d252..a74bdd9c 100644 --- a/EcommerceApi.DiegoPetrola/Migrations/20260209234942_Init.cs +++ b/EcommerceApi.DiegoPetrola/Migrations/20260216042746_Initial.cs @@ -6,7 +6,7 @@ namespace EcommerceApi.Migrations { /// - public partial class Init : Migration + public partial class Initial : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -17,8 +17,7 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), - Name = table.Column(type: "nvarchar(450)", nullable: false), - IsDeleted = table.Column(type: "bit", nullable: false) + Name = table.Column(type: "nvarchar(450)", nullable: false) }, constraints: table => { @@ -31,7 +30,8 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), - SaleDate = table.Column(type: "datetime2", nullable: false) + SaleDate = table.Column(type: "datetime2", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) }, constraints: table => { @@ -46,7 +46,8 @@ protected override void Up(MigrationBuilder migrationBuilder) .Annotation("SqlServer:Identity", "1, 1"), Name = table.Column(type: "nvarchar(max)", nullable: false), Price = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), - CategoryId = table.Column(type: "int", nullable: false) + CategoryId = table.Column(type: "int", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) }, constraints: table => { @@ -65,6 +66,8 @@ protected override void Up(MigrationBuilder migrationBuilder) { ProductId = table.Column(type: "int", nullable: false), SaleId = table.Column(type: "int", nullable: false), + ProductName = table.Column(type: "nvarchar(max)", nullable: false), + ProductPrice = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), Quantity = table.Column(type: "int", nullable: false) }, constraints: table => diff --git a/EcommerceApi.DiegoPetrola/Migrations/EcommerceDbContextModelSnapshot.cs b/EcommerceApi.DiegoPetrola/Migrations/EcommerceDbContextModelSnapshot.cs index dc6ee51b..8b733f3c 100644 --- a/EcommerceApi.DiegoPetrola/Migrations/EcommerceDbContextModelSnapshot.cs +++ b/EcommerceApi.DiegoPetrola/Migrations/EcommerceDbContextModelSnapshot.cs @@ -30,9 +30,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("IsDeleted") - .HasColumnType("bit"); - b.Property("Name") .IsRequired() .HasColumnType("nvarchar(450)"); diff --git a/EcommerceApi.DiegoPetrola/Models/Category.cs b/EcommerceApi.DiegoPetrola/Models/Category.cs index d8364eae..53b31579 100644 --- a/EcommerceApi.DiegoPetrola/Models/Category.cs +++ b/EcommerceApi.DiegoPetrola/Models/Category.cs @@ -6,10 +6,9 @@ public class Category { public int Id { get; set; } public string Name { get; set; } = string.Empty; - public bool IsDeleted { get; set; } = false; public ICollection Products { get; set; } = []; public CategoryDto ToDto() { - return new CategoryDto(Id, Name, IsDeleted); + return new CategoryDto(Id, Name); } } diff --git a/EcommerceApi.DiegoPetrola/Models/DTOs/CommerceDtos.cs b/EcommerceApi.DiegoPetrola/Models/DTOs/CommerceDtos.cs index 53f56560..250005ae 100644 --- a/EcommerceApi.DiegoPetrola/Models/DTOs/CommerceDtos.cs +++ b/EcommerceApi.DiegoPetrola/Models/DTOs/CommerceDtos.cs @@ -1,6 +1,6 @@ namespace EcommerceApi.Models.DTOs; -public record CategoryDto(int Id, string Name, bool IsDeleted); +public record CategoryDto(int Id, string Name); public record CreateCategoryDto(string Name); public record ProductDto(int Id, string Name, decimal Price, int CategoryId, string CategoryName); public record CreateProductDto(string Name, decimal Price, int CategoryId); diff --git a/EcommerceApi.DiegoPetrola/Results/GlobalExceptionHandler.cs b/EcommerceApi.DiegoPetrola/Results/GlobalExceptionHandler.cs deleted file mode 100644 index 07de9417..00000000 --- a/EcommerceApi.DiegoPetrola/Results/GlobalExceptionHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace EcommerceApi.Results; - -using EcommerceApi.Errors; -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Mvc; - -public class GlobalExceptionHandler(ILogger logger) : IExceptionHandler -{ - private readonly ILogger _logger = logger; - - public async ValueTask TryHandleAsync( - HttpContext httpContext, - Exception exception, - CancellationToken cancellationToken) - { - _logger.LogError(exception, "An unhandled exception occurred."); - - var (statusCode, title) = exception switch - { - UnauthorizedAccessException => (StatusCodes.Status401Unauthorized, "Unauthorized"), - ArgumentException => (StatusCodes.Status400BadRequest, "Bad Request"), - NotFoundException => (StatusCodes.Status404NotFound, "Not Found"), - _ => (StatusCodes.Status500InternalServerError, "Internal Server Error") - }; - - var problemDetails = new ProblemDetails - { - Status = statusCode, - Title = title, - Detail = statusCode < 500 ? exception.Message : exception.Message - }; - - httpContext.Response.StatusCode = statusCode; - - await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken); - - return true; - } -} - diff --git a/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs index 1ae11ac4..e3e17d49 100644 --- a/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs +++ b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs @@ -30,9 +30,16 @@ public async Task> GetCategory(int id) var category = await context.Categories.FindAsync(id); if (category is null) return Result.NotFound("Category not found."); - category.IsDeleted = true; - await context.SaveChangesAsync(); - return Result.Ok(null); + try + { + context.Remove(category); + await context.SaveChangesAsync(); + return Result.Ok(null); + } + catch + { + return Result.InternalServerError("Problem on the server"); + } } public async Task> CreateCategory(CreateCategoryDto dto) diff --git a/EcommerceApi.DiegoPetrola/Services/ProductsService.cs b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs index 71094b9c..3cc4e90c 100644 --- a/EcommerceApi.DiegoPetrola/Services/ProductsService.cs +++ b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs @@ -49,11 +49,32 @@ await context.Entry(product) .Reference(p => p.Category) .LoadAsync(); } - catch (DbUpdateException) + catch { return Result.Invalid("Invalid product data or duplicate entry"); } + return Result.Ok(product.ToDto()); + } + + public async Task> UpdateProduct(ProductDto dto) + { + var product = await context.Products.FindAsync(dto.Id); + + if (product is null) + return Result.NotFound("Product not found"); + + try + { + product.CategoryId = dto.CategoryId; + product.Name = dto.Name; + await context.SaveChangesAsync(); + await context.Entry(product).Reference(p => p.Category).LoadAsync(); + } + catch + { + return Result.Invalid("Something went wrong"); + } return Result.Ok(product.ToDto()); } @@ -63,8 +84,15 @@ await context.Entry(product) if (product is null) return Result.NotFound("Product not found"); - product.IsDeleted = true; - await context.SaveChangesAsync(); - return Result.Ok(null); + try + { + product.IsDeleted = true; + await context.SaveChangesAsync(); + return Result.Ok(null); + } + catch + { + return Result.InternalServerError("Something went wrong"); + } } } diff --git a/EcommerceApi.DiegoPetrola/Services/SalesService.cs b/EcommerceApi.DiegoPetrola/Services/SalesService.cs index a46a866a..5ad09c2f 100644 --- a/EcommerceApi.DiegoPetrola/Services/SalesService.cs +++ b/EcommerceApi.DiegoPetrola/Services/SalesService.cs @@ -11,7 +11,7 @@ public class SalesService(EcommerceDbContext context) public async Task>> GetSalesByPage(int page) { var sales = await context.Sales - .IgnoreQueryFilters() + .Include(s => s.SaleItems) .Skip(page * 20) .Take(20) .Select(s => s.ToDto()) @@ -23,7 +23,6 @@ public async Task>> GetSalesByPage(int page) public async Task> GetSale(int id) { var sale = await context.Sales - .IgnoreQueryFilters() .Include(s => s.SaleItems) .ThenInclude(si => si.Product) .FirstOrDefaultAsync(s => s.Id == id); @@ -53,10 +52,16 @@ public async Task> CreateSale(CreateSaleDto dto) ProductPrice = product.Price }); } - - context.Sales.Add(sale); - await context.SaveChangesAsync(); - return Result.Ok(sale.ToDto()); + try + { + context.Sales.Add(sale); + await context.SaveChangesAsync(); + return Result.Ok(sale.ToDto()); + } + catch + { + return Result.InternalServerError("Something went wrong"); + } } public async Task> DeleteSale(int id) From 05cc90aac486a6872c1cc5ef11385d81992402bf Mon Sep 17 00:00:00 2001 From: Diego Leal Date: Mon, 16 Feb 2026 02:37:01 -0300 Subject: [PATCH 09/13] added README --- .../Controllers/CategoriesController.cs | 4 +-- ...json => Ecommerce.postman_collection.json} | 0 EcommerceApi.DiegoPetrola/Errors/Errors.cs | 3 -- EcommerceApi.DiegoPetrola/Models/Category.cs | 8 +---- EcommerceApi.DiegoPetrola/Models/Product.cs | 8 +---- EcommerceApi.DiegoPetrola/Models/Sale.cs | 13 +------ EcommerceApi.DiegoPetrola/Models/SaleItem.cs | 9 +---- .../Services/CategoriesService.cs | 8 +++-- .../Services/ProductsService.cs | 1 + .../Services/SalesService.cs | 13 +++++-- .../Utils/CategoryMapping.cs | 33 +++++++++++++++++ README.md | 35 +++++++++++++++++++ 12 files changed, 92 insertions(+), 43 deletions(-) rename EcommerceApi.DiegoPetrola/{Dotnet Ecommerce.postman_collection.json => Ecommerce.postman_collection.json} (100%) delete mode 100644 EcommerceApi.DiegoPetrola/Errors/Errors.cs create mode 100644 EcommerceApi.DiegoPetrola/Utils/CategoryMapping.cs create mode 100644 README.md diff --git a/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs b/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs index d75d082a..6755bd8f 100644 --- a/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs +++ b/EcommerceApi.DiegoPetrola/Controllers/CategoriesController.cs @@ -30,7 +30,7 @@ public async Task> CreateCategory(CreateCategoryDto dt } [HttpPut("{id}")] - public async Task DeleteCategory(int id, CategoryDto dto) + public async Task UpdateCategory(int id, CategoryDto dto) { if (dto.Id != id) return BadRequest(); @@ -41,7 +41,7 @@ public async Task DeleteCategory(int id, CategoryDto dto) [HttpDelete("{id}")] public async Task DeleteCategory(int id) { - var res = await service.SoftDeleteCategory(id); + var res = await service.DeleteCategory(id); return this.ToActionResult(res); } } diff --git a/EcommerceApi.DiegoPetrola/Dotnet Ecommerce.postman_collection.json b/EcommerceApi.DiegoPetrola/Ecommerce.postman_collection.json similarity index 100% rename from EcommerceApi.DiegoPetrola/Dotnet Ecommerce.postman_collection.json rename to EcommerceApi.DiegoPetrola/Ecommerce.postman_collection.json diff --git a/EcommerceApi.DiegoPetrola/Errors/Errors.cs b/EcommerceApi.DiegoPetrola/Errors/Errors.cs deleted file mode 100644 index bfe6ebb5..00000000 --- a/EcommerceApi.DiegoPetrola/Errors/Errors.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace EcommerceApi.Errors; - -public class NotFoundException(string message) : Exception(message) { } diff --git a/EcommerceApi.DiegoPetrola/Models/Category.cs b/EcommerceApi.DiegoPetrola/Models/Category.cs index 53b31579..25a726a3 100644 --- a/EcommerceApi.DiegoPetrola/Models/Category.cs +++ b/EcommerceApi.DiegoPetrola/Models/Category.cs @@ -1,14 +1,8 @@ -using EcommerceApi.Models.DTOs; - -namespace EcommerceApi.Models; +namespace EcommerceApi.Models; public class Category { public int Id { get; set; } public string Name { get; set; } = string.Empty; public ICollection Products { get; set; } = []; - public CategoryDto ToDto() - { - return new CategoryDto(Id, Name); - } } diff --git a/EcommerceApi.DiegoPetrola/Models/Product.cs b/EcommerceApi.DiegoPetrola/Models/Product.cs index 0a8b7428..7182c53f 100644 --- a/EcommerceApi.DiegoPetrola/Models/Product.cs +++ b/EcommerceApi.DiegoPetrola/Models/Product.cs @@ -1,6 +1,4 @@ -using EcommerceApi.Models.DTOs; - -namespace EcommerceApi.Models; +namespace EcommerceApi.Models; public class Product { @@ -11,8 +9,4 @@ public class Product public bool IsDeleted { get; set; } = false; public Category Category { get; set; } = null!; public ICollection SaleItems { get; set; } = []; - public ProductDto ToDto() - { - return new ProductDto(Id, Name, Price, Category.Id, Category.Name); - } } diff --git a/EcommerceApi.DiegoPetrola/Models/Sale.cs b/EcommerceApi.DiegoPetrola/Models/Sale.cs index 42ccd77b..4fe3b2b1 100644 --- a/EcommerceApi.DiegoPetrola/Models/Sale.cs +++ b/EcommerceApi.DiegoPetrola/Models/Sale.cs @@ -1,6 +1,4 @@ -using EcommerceApi.Models.DTOs; - -namespace EcommerceApi.Models; +namespace EcommerceApi.Models; public class Sale { @@ -8,13 +6,4 @@ public class Sale public DateTime SaleDate { get; set; } public bool IsDeleted { get; set; } = false; public ICollection SaleItems { get; set; } = []; - public SaleDto ToDto() - { - return new SaleDto( - Id, - SaleDate, - [.. SaleItems.Select(si => si.ToDto())], - SaleItems.Sum(si => si.Quantity * si.ProductPrice) - ); - } } diff --git a/EcommerceApi.DiegoPetrola/Models/SaleItem.cs b/EcommerceApi.DiegoPetrola/Models/SaleItem.cs index ab6e47c1..cd1c9709 100644 --- a/EcommerceApi.DiegoPetrola/Models/SaleItem.cs +++ b/EcommerceApi.DiegoPetrola/Models/SaleItem.cs @@ -1,6 +1,4 @@ -using EcommerceApi.Models.DTOs; - -namespace EcommerceApi.Models; +namespace EcommerceApi.Models; public class SaleItem { @@ -11,9 +9,4 @@ public class SaleItem public int SaleId { get; set; } public Sale Sale { get; set; } = null!; public int Quantity { get; set; } - - public SaleItemDto ToDto() - { - return new SaleItemDto(ProductId, ProductName, Quantity, ProductPrice); - } } diff --git a/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs index e3e17d49..223e5e1b 100644 --- a/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs +++ b/EcommerceApi.DiegoPetrola/Services/CategoriesService.cs @@ -2,6 +2,7 @@ using EcommerceApi.Models; using EcommerceApi.Models.DTOs; using EcommerceApi.Results; +using EcommerceApi.Utils; using Microsoft.EntityFrameworkCore; namespace EcommerceApi.Services; @@ -25,11 +26,14 @@ public async Task> GetCategory(int id) return Result.Ok(category.ToDto()); } - public async Task> SoftDeleteCategory(int id) + public async Task> DeleteCategory(int id) { var category = await context.Categories.FindAsync(id); + var products = await context.Products.AnyAsync(p => p.CategoryId == id); + if (products) + return Result.Invalid("Can't delete a category that has products"); if (category is null) - return Result.NotFound("Category not found."); + return Result.NotFound("Category not found"); try { context.Remove(category); diff --git a/EcommerceApi.DiegoPetrola/Services/ProductsService.cs b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs index 3cc4e90c..22477df5 100644 --- a/EcommerceApi.DiegoPetrola/Services/ProductsService.cs +++ b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs @@ -2,6 +2,7 @@ using EcommerceApi.Models; using EcommerceApi.Models.DTOs; using EcommerceApi.Results; +using EcommerceApi.Utils; using Microsoft.EntityFrameworkCore; namespace EcommerceApi.Services; diff --git a/EcommerceApi.DiegoPetrola/Services/SalesService.cs b/EcommerceApi.DiegoPetrola/Services/SalesService.cs index 5ad09c2f..8b1f5b1b 100644 --- a/EcommerceApi.DiegoPetrola/Services/SalesService.cs +++ b/EcommerceApi.DiegoPetrola/Services/SalesService.cs @@ -2,6 +2,7 @@ using EcommerceApi.Models; using EcommerceApi.Models.DTOs; using EcommerceApi.Results; +using EcommerceApi.Utils; using Microsoft.EntityFrameworkCore; namespace EcommerceApi.Services; @@ -38,11 +39,19 @@ public async Task> CreateSale(CreateSaleDto dto) return Result.Invalid("Sale must contain at least one item."); var sale = new Sale { SaleDate = DateTime.UtcNow }; + var productIds = dto.Items.Select(i => i.ProductId).ToList(); + var products = await context.Products + .Where(p => productIds.Contains(p.Id)) + .ToDictionaryAsync(p => p.Id); + + if (products.Count < productIds.Count) + return Result.Invalid($"Can't make sale of invalid products!"); foreach (var item in dto.Items) { - var product = await context.Products.FindAsync(item.ProductId); - if (product == null) return Result.Invalid($"Can't make sale of invalid product {item.ProductId}!"); + products.TryGetValue(item.ProductId, out var product); + if (product is null) + return Result.Invalid($"Can't make sale of invalid products!"); sale.SaleItems.Add(new SaleItem { diff --git a/EcommerceApi.DiegoPetrola/Utils/CategoryMapping.cs b/EcommerceApi.DiegoPetrola/Utils/CategoryMapping.cs new file mode 100644 index 00000000..1f2e26cc --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Utils/CategoryMapping.cs @@ -0,0 +1,33 @@ +using EcommerceApi.Models; +using EcommerceApi.Models.DTOs; + +namespace EcommerceApi.Utils; + +public static class CategoryMapping +{ + public static CategoryDto ToDto(this Category category) + { + return new CategoryDto(category.Id, category.Name); + } + + public static ProductDto ToDto(this Product product) + { + return new ProductDto(product.Id, product.Name, product.Price, product.Category.Id, product.Category.Name); + } + + public static SaleItemDto ToDto(this SaleItem saleItem) + { + return new SaleItemDto(saleItem.ProductId, saleItem.ProductName, saleItem.Quantity, saleItem.ProductPrice); + } + + public static SaleDto ToDto(this Sale sale) + { + return new SaleDto( + sale.Id, + sale.SaleDate, + [.. sale.SaleItems.Select(si => si.ToDto())], + sale.SaleItems.Sum(si => si.Quantity * si.ProductPrice) + ); + } + +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..5422384c --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Ecommerce API + +A RESTful API built with **ASP.NET Core** and **Entity Framework Core**, designed to simulate a retail environment. It uses use good development practices such as Result Pattern, Service Layer, Data Transfer Objects (DTOs), Soft Deletes and historical data integrity. + +## Features + +- **Product, Category and Sales Management**: Full CRUD capabilities with relationship validation. +- **Historical Price Integrity**: Implements "snapshotting" logic to ensure past sales records remain accurate even if current product prices change. +- **Soft Deletes**: Implements non-destructive deletion for Products, and Sales to preserve data history, **Categories** can be deleted only there are no Products using them. +- **Pagination**: Endpoint pagination using performant `Skip`/`Take` logic. +- **Result Pattern**: Uses a functional `Result` wrapper to handle service-layer errors gracefully without relying on expensive Exception throwing for control flow. + +## Tech Stack + +- **Framework**: .NET 10 (ASP.NET Core Web API) +- **ORM**: Entity Framework Core +- **Database**: SQL Server +- **Documentation**: OpenAPI +- **Architecture**: N-Layer (Controller -> Service -> Data Access) + +## Installation + +1. **Clone the repository** + + ```bash + git clone https://github.com/diegopetrola/CodeReviews.Console.EcommerceApi + cd CodeReviews.Console.EcommerceApi + ``` + +2. **Install libraries**: `dotnet restore` +3. **Update the Database**: `dotnet ef database update` + +4. **Run the project** `dotnet run` + +5. **Test on Postman**: import `Ecommerce.postman_collection.json` on Postman and run collection. From 11392691c637906667a24e58219f640e814a917c Mon Sep 17 00:00:00 2001 From: Diego Leal Date: Mon, 16 Feb 2026 03:02:35 -0300 Subject: [PATCH 10/13] removed hard-coded number for pagination --- .../Controllers/ProductsController.cs | 11 +++++++---- .../Controllers/SalesController.cs | 11 +++++++---- EcommerceApi.DiegoPetrola/Program.cs | 8 +++----- EcommerceApi.DiegoPetrola/Services/ProductsService.cs | 6 +++--- EcommerceApi.DiegoPetrola/Services/SalesService.cs | 6 +++--- EcommerceApi.DiegoPetrola/Utils/PaginationSettings.cs | 7 +++++++ EcommerceApi.DiegoPetrola/appsettings.json | 4 ++++ 7 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 EcommerceApi.DiegoPetrola/Utils/PaginationSettings.cs diff --git a/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs b/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs index de6dedaa..52669067 100644 --- a/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs +++ b/EcommerceApi.DiegoPetrola/Controllers/ProductsController.cs @@ -1,17 +1,20 @@ using EcommerceApi.Models.DTOs; using EcommerceApi.Services; +using EcommerceApi.Utils; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; namespace EcommerceApi.Controllers; [ApiController] [Route("api/[controller]")] -public class ProductsController(ProductsService service) : ControllerBase +public class ProductsController(ProductsService service, IOptions options) : ControllerBase { - [HttpGet("page/{page}")] - public async Task>> GetProducts(int page) + [HttpGet] + public async Task>> GetProducts([FromQuery] int? pageSize, [FromQuery] int pageNumber = 0) { - var res = await service.GetProducts(page); + int finalSize = Math.Min(pageSize ?? options.Value.DefaultPageSize, options.Value.MaxPageSize); + var res = await service.GetProducts(pageNumber, finalSize); return this.ToActionResult(res); } diff --git a/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs b/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs index 65786d20..7a9c587f 100644 --- a/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs +++ b/EcommerceApi.DiegoPetrola/Controllers/SalesController.cs @@ -1,12 +1,14 @@ using EcommerceApi.Models.DTOs; using EcommerceApi.Services; +using EcommerceApi.Utils; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; namespace EcommerceApi.Controllers; [ApiController] [Route("api/[controller]")] -public class SalesController(SalesService service) : ControllerBase +public class SalesController(SalesService service, IOptions options) : ControllerBase { [HttpGet("{id}")] public async Task> GetSale(int id) @@ -15,10 +17,11 @@ public async Task> GetSale(int id) return this.ToActionResult(res); } - [HttpGet("page/{page}")] - public async Task>> GetSalePage(int page) + [HttpGet()] + public async Task>> GetSalePage([FromQuery] int? pageSize, [FromQuery] int pageNumber = 0) { - var res = await service.GetSalesByPage(page); + int finalSize = Math.Min(pageSize ?? options.Value.DefaultPageSize, options.Value.MaxPageSize); + var res = await service.GetSalesByPage(pageNumber, finalSize); return this.ToActionResult(res); } diff --git a/EcommerceApi.DiegoPetrola/Program.cs b/EcommerceApi.DiegoPetrola/Program.cs index c02ed50c..39fc326a 100644 --- a/EcommerceApi.DiegoPetrola/Program.cs +++ b/EcommerceApi.DiegoPetrola/Program.cs @@ -1,17 +1,15 @@ using EcommerceApi.Context; using EcommerceApi.Services; +using EcommerceApi.Utils; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")) - // TODO generate seeding later - //.UseAsyncSeeding(async (context, _, CancellationToken) => - //{ - // await DatabaseSeeding.CustomSeeding((EcommerceDbContext)context); - //}) ); +builder.Services.Configure( + builder.Configuration.GetSection("PaginationSettings")); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/EcommerceApi.DiegoPetrola/Services/ProductsService.cs b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs index 22477df5..a266489f 100644 --- a/EcommerceApi.DiegoPetrola/Services/ProductsService.cs +++ b/EcommerceApi.DiegoPetrola/Services/ProductsService.cs @@ -9,12 +9,12 @@ namespace EcommerceApi.Services; public class ProductsService(EcommerceDbContext context) { - public async Task>> GetProducts(int page) + public async Task>> GetProducts(int pageNumber, int pageSize) { var products = await context.Products .Include(p => p.Category) - .Skip(20 * page) - .Take(20) + .Skip(pageSize * pageNumber) + .Take(pageSize) .Select(p => p.ToDto()) .ToListAsync(); diff --git a/EcommerceApi.DiegoPetrola/Services/SalesService.cs b/EcommerceApi.DiegoPetrola/Services/SalesService.cs index 8b1f5b1b..a74ba28f 100644 --- a/EcommerceApi.DiegoPetrola/Services/SalesService.cs +++ b/EcommerceApi.DiegoPetrola/Services/SalesService.cs @@ -9,12 +9,12 @@ namespace EcommerceApi.Services; public class SalesService(EcommerceDbContext context) { - public async Task>> GetSalesByPage(int page) + public async Task>> GetSalesByPage(int pageNumber, int pageSize) { var sales = await context.Sales .Include(s => s.SaleItems) - .Skip(page * 20) - .Take(20) + .Skip(pageSize * pageNumber) + .Take(pageSize) .Select(s => s.ToDto()) .ToListAsync(); diff --git a/EcommerceApi.DiegoPetrola/Utils/PaginationSettings.cs b/EcommerceApi.DiegoPetrola/Utils/PaginationSettings.cs new file mode 100644 index 00000000..1b2ea6c8 --- /dev/null +++ b/EcommerceApi.DiegoPetrola/Utils/PaginationSettings.cs @@ -0,0 +1,7 @@ +namespace EcommerceApi.Utils; + +public class PaginationSettings +{ + public int DefaultPageSize { get; set; } + public int MaxPageSize { get; set; } +} diff --git a/EcommerceApi.DiegoPetrola/appsettings.json b/EcommerceApi.DiegoPetrola/appsettings.json index 1e18fc9b..bbc17264 100644 --- a/EcommerceApi.DiegoPetrola/appsettings.json +++ b/EcommerceApi.DiegoPetrola/appsettings.json @@ -8,5 +8,9 @@ "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CommerceDB;Trusted_Connection=True;TrustServerCertificate=True;" + }, + "PaginationSettings": { + "DefaultPageSize": 20, + "MaxPageSize": 100 } } From c117b32fe1b31bb9a0ecd58f76314823153a2113 Mon Sep 17 00:00:00 2001 From: Diego Leal Date: Mon, 16 Feb 2026 03:04:40 -0300 Subject: [PATCH 11/13] Updated README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5422384c..91120f24 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A RESTful API built with **ASP.NET Core** and **Entity Framework Core**, designe - **Product, Category and Sales Management**: Full CRUD capabilities with relationship validation. - **Historical Price Integrity**: Implements "snapshotting" logic to ensure past sales records remain accurate even if current product prices change. - **Soft Deletes**: Implements non-destructive deletion for Products, and Sales to preserve data history, **Categories** can be deleted only there are no Products using them. -- **Pagination**: Endpoint pagination using performant `Skip`/`Take` logic. +- **Pagination**: Endpoint pagination using performant `Skip`/`Take` logic and configurable page size via query parameters that prevent abuse or DDoS. - **Result Pattern**: Uses a functional `Result` wrapper to handle service-layer errors gracefully without relying on expensive Exception throwing for control flow. ## Tech Stack From e77f050bf2e494c04620f16147cf7cf5dad6e1e9 Mon Sep 17 00:00:00 2001 From: Diego Leal Date: Mon, 16 Feb 2026 03:08:20 -0300 Subject: [PATCH 12/13] updated postman file --- .../Ecommerce.postman_collection.json | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/EcommerceApi.DiegoPetrola/Ecommerce.postman_collection.json b/EcommerceApi.DiegoPetrola/Ecommerce.postman_collection.json index 1c8302d0..02a44e38 100644 --- a/EcommerceApi.DiegoPetrola/Ecommerce.postman_collection.json +++ b/EcommerceApi.DiegoPetrola/Ecommerce.postman_collection.json @@ -254,19 +254,34 @@ "item": [ { "name": "Get Prod Page", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, "request": { "method": "GET", "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [] + }, "url": { - "raw": "{{base_url}}/api/products/page/0", + "raw": "{{base_url}}/api/products?pageSize=1&pageNumber=0", "host": [ "{{base_url}}" ], "path": [ "api", - "products", - "page", - "0" + "products" + ], + "query": [ + { + "key": "pageSize", + "value": "1" + }, + { + "key": "pageNumber", + "value": "0" + } ] } }, @@ -577,14 +592,14 @@ "raw": "" }, "url": { - "raw": "{{base_url}}/api/sales/1", + "raw": "{{base_url}}/api/sales/3", "host": [ "{{base_url}}" ], "path": [ "api", "sales", - "1" + "3" ] } }, @@ -603,15 +618,13 @@ "raw": "" }, "url": { - "raw": "{{base_url}}/api/sales/page/0", + "raw": "{{base_url}}/api/sales", "host": [ "{{base_url}}" ], "path": [ "api", - "sales", - "page", - "0" + "sales" ] } }, From 50024f32ca79d62ce4ad89885ae7d9b800f7dfc5 Mon Sep 17 00:00:00 2001 From: Diego Leal Date: Mon, 16 Feb 2026 03:49:43 -0300 Subject: [PATCH 13/13] fix codacy errors --- EcommerceApi.DiegoPetrola/Models/Product.cs | 2 +- EcommerceApi.DiegoPetrola/Models/Sale.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/EcommerceApi.DiegoPetrola/Models/Product.cs b/EcommerceApi.DiegoPetrola/Models/Product.cs index 7182c53f..96cf6783 100644 --- a/EcommerceApi.DiegoPetrola/Models/Product.cs +++ b/EcommerceApi.DiegoPetrola/Models/Product.cs @@ -6,7 +6,7 @@ public class Product public string Name { get; set; } = string.Empty; public decimal Price { get; set; } public int CategoryId { get; set; } - public bool IsDeleted { get; set; } = false; + public bool IsDeleted { get; set; } public Category Category { get; set; } = null!; public ICollection SaleItems { get; set; } = []; } diff --git a/EcommerceApi.DiegoPetrola/Models/Sale.cs b/EcommerceApi.DiegoPetrola/Models/Sale.cs index 4fe3b2b1..88eda93f 100644 --- a/EcommerceApi.DiegoPetrola/Models/Sale.cs +++ b/EcommerceApi.DiegoPetrola/Models/Sale.cs @@ -4,6 +4,6 @@ public class Sale { public int Id { get; set; } public DateTime SaleDate { get; set; } - public bool IsDeleted { get; set; } = false; + public bool IsDeleted { get; set; } public ICollection SaleItems { get; set; } = []; }