diff --git a/.gitignore b/.gitignore index a62e968..b2e042c 100644 --- a/.gitignore +++ b/.gitignore @@ -482,3 +482,4 @@ $RECYCLE.BIN/ */**/obj/Release /exercise.pizzashopapi/appsettings.json /exercise.pizzashopapi/appsettings.Development.json +*/**/Migrations \ No newline at end of file diff --git a/exercise.pizzashopapi/DTO/Customer.cs b/exercise.pizzashopapi/DTO/Customer.cs new file mode 100644 index 0000000..5a125aa --- /dev/null +++ b/exercise.pizzashopapi/DTO/Customer.cs @@ -0,0 +1,6 @@ +namespace exercise.pizzashopapi.DTO +{ + public record CustomerPost(string Name); + public record CustomerView(int Id, string Name, IEnumerable Orders); + public record CustomerInternal(int Id, string Name); +} diff --git a/exercise.pizzashopapi/DTO/Order.cs b/exercise.pizzashopapi/DTO/Order.cs new file mode 100644 index 0000000..3d2d5b9 --- /dev/null +++ b/exercise.pizzashopapi/DTO/Order.cs @@ -0,0 +1,25 @@ +namespace exercise.pizzashopapi.DTO +{ + public record OrderPost(int ProductId, int CustomerId); + public record OrderPostAddTopping(int ToppingId); + public record OrderView( + int Id, decimal TotalPrice, bool IsDelivered, string PreparationStage, + DateTime CreatedAt, DateTime StartedAt, DateTime CompletedAt, + CustomerInternal Customer, ProductView Product, IEnumerable Toppings + ); + public record OrderProduct( + int Id, decimal TotalPrice, bool IsDelivered, string PreparationStage, + DateTime CreatedAt, DateTime StartedAt, DateTime CompletedAt, + ProductView Product + ); + public record OrderProductToppings( + int Id, decimal TotalPrice, bool IsDelivered, string PreparationStage, + DateTime CreatedAt, DateTime StartedAt, DateTime CompletedAt, + ProductView Product, IEnumerable Toppings + ); + public record OrderCustomerProduct( + int Id, decimal TotalPrice, bool IsDelivered, string PreparationStage, + DateTime CreatedAt, DateTime StartedAt, DateTime CompletedAt, + CustomerInternal Customer, ProductView Product + ); +} diff --git a/exercise.pizzashopapi/DTO/Product.cs b/exercise.pizzashopapi/DTO/Product.cs new file mode 100644 index 0000000..1831359 --- /dev/null +++ b/exercise.pizzashopapi/DTO/Product.cs @@ -0,0 +1,4 @@ +namespace exercise.pizzashopapi.DTO +{ + public record ProductView(int Id, string ProductType, string Name, decimal Price); +} diff --git a/exercise.pizzashopapi/DTO/Topping.cs b/exercise.pizzashopapi/DTO/Topping.cs new file mode 100644 index 0000000..97d8e6b --- /dev/null +++ b/exercise.pizzashopapi/DTO/Topping.cs @@ -0,0 +1,5 @@ +namespace exercise.pizzashopapi.DTO +{ + public record ToppingView(int Id, string Name, decimal Price, IEnumerable Orders); + public record ToppingInternal(int Id, string Name, decimal Price); +} diff --git a/exercise.pizzashopapi/Data/DataContext.cs b/exercise.pizzashopapi/Data/DataContext.cs index 129199e..d24c656 100644 --- a/exercise.pizzashopapi/Data/DataContext.cs +++ b/exercise.pizzashopapi/Data/DataContext.cs @@ -3,26 +3,37 @@ namespace exercise.pizzashopapi.Data { - public class DataContext : DbContext + public class DataContext(DbContextOptions options) : DbContext(options) { - private string connectionString; - public DataContext() + protected override void OnModelCreating(ModelBuilder modelBuilder) { - var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); - connectionString = configuration.GetValue("ConnectionStrings:DefaultConnectionString"); + modelBuilder.Entity().HasKey(ot => new { ot.OrderId, ot.ToppingId }); + modelBuilder.Entity() + .HasMany(o => o.Toppings) + .WithMany(t => t.Orders) + .UsingEntity(); + modelBuilder.Entity() + .HasOne(o => o.Product) + .WithMany(p => p.Orders) + .HasForeignKey(o => o.ProductId) + .OnDelete(DeleteBehavior.SetNull); + modelBuilder.Entity() + .HasOne(o => o.Customer) + .WithMany(c => c.Orders) + .HasForeignKey(o => o.CustomerId) + .OnDelete(DeleteBehavior.SetNull); + Seeder seeder = new Seeder(); + modelBuilder.Entity().HasData(seeder.Customers); + modelBuilder.Entity().HasData(seeder.Products); + modelBuilder.Entity().HasData(seeder.Orders); + modelBuilder.Entity().HasData(seeder.Toppings); + modelBuilder.Entity().HasData(seeder.OrderToppings); } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseNpgsql(connectionString); - //set primary of order? - - //seed data? - - } - public DbSet Pizzas { get; set; } + public DbSet Products { get; set; } public DbSet Customers { get; set; } public DbSet Orders { get; set; } + public DbSet Toppings { get; set; } } } diff --git a/exercise.pizzashopapi/Data/Seeder.cs b/exercise.pizzashopapi/Data/Seeder.cs index f87fbef..eae0cbe 100644 --- a/exercise.pizzashopapi/Data/Seeder.cs +++ b/exercise.pizzashopapi/Data/Seeder.cs @@ -1,34 +1,58 @@ using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Enums; namespace exercise.pizzashopapi.Data { - public static class Seeder + public class Seeder { - public async static void SeedPizzaShopApi(this WebApplication app) - { - using(var db = new DataContext()) - { - if(!db.Customers.Any()) - { - db.Add(new Customer() { Name="Nigel" }); - db.Add(new Customer() { Name = "Dave" }); - await db.SaveChangesAsync(); - } - if(!db.Pizzas.Any()) - { - db.Add(new Pizza() { Name = "Cheese & Pineapple" }); - db.Add(new Pizza() { Name = "Vegan Cheese Tastic" }); - await db.SaveChangesAsync(); + private List _customers = [ + new Customer { Id = 1, Name = "Nigel"}, + new Customer { Id = 2, Name = "Dave"}, + new Customer { Id = 3, Name = "Nikolai"} + ]; - } + private List _products = [ + new Product { Id = 1, ProductType = ProductType.Pizza, Name = "Hawaiian Pizza", Price = 200 }, + new Product { Id = 2, ProductType = ProductType.Pizza, Name = "Vegan Cheese Tastic", Price = 165 }, + new Product { Id = 3, ProductType = ProductType.Pizza, Name = "Pizza Vendetta", Price = 210 }, + new Product { Id = 4, ProductType = ProductType.Burger, Name = "Bacon Cheese Burger", Price = 195 }, + new Product { Id = 5, ProductType = ProductType.Fries, Name = "Regular Fries", Price = 110 }, + new Product { Id = 6, ProductType = ProductType.Drinks, Name = "Coca Cola 0.4L", Price = 65 }, + new Product { Id = 7, ProductType = ProductType.Drinks, Name = "Fanta Orange 0.4L", Price = 65 }, + ]; - //order data - if(1==1) - { + private List _orders = [ + new Order { + Id = 1, CustomerId = 1, ProductId = 2, IsDelivered = true, + CreatedAt = DateTime.UtcNow.AddDays(-1), StartedAt = DateTime.UtcNow.AddDays(-1).AddMinutes(4), + CompletedAt = DateTime.UtcNow.AddDays(-1).AddMinutes(12), + PreparationStage = PreparationStage.Finished }, + new Order { Id = 2, CustomerId = 2, ProductId = 1, IsDelivered = false }, + new Order { Id = 3, CustomerId = 3, ProductId = 3, IsDelivered = true, + CreatedAt = DateTime.UtcNow.AddDays(-1), StartedAt = DateTime.UtcNow.AddDays(-1).AddMinutes(4), + CompletedAt = DateTime.UtcNow.AddDays(-1).AddMinutes(12), + PreparationStage = PreparationStage.Finished}, + new Order { Id = 4, CustomerId = 3, ProductId = 7, IsDelivered = true, + CreatedAt = DateTime.UtcNow.AddDays(-1), StartedAt = DateTime.UtcNow.AddDays(-1).AddMinutes(4), + CompletedAt = DateTime.UtcNow.AddDays(-1).AddMinutes(12), + PreparationStage = PreparationStage.Finished}, + ]; - await db.SaveChangesAsync(); - } - } - } + private List _toppings = [ + new Topping {Id = 1, Name = "Bacon", Price = 20 }, + new Topping {Id = 2, Name = "Onion Rings", Price = 15 }, + new Topping {Id = 3, Name = "Hot Sauce", Price = 12 }, + ]; + + private List _orderToppings = [ + new OrderTopping { OrderId = 3, ToppingId = 3 }, + new OrderTopping { OrderId = 1, ToppingId = 1 } + ]; + + public List Customers { get { return _customers; } } + public List Products { get { return _products; } } + public List Orders { get { return _orders; } } + public List Toppings { get { return _toppings; } } + public List OrderToppings { get { return _orderToppings; } } } } diff --git a/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs b/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs deleted file mode 100644 index f8be2b0..0000000 --- a/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs +++ /dev/null @@ -1,15 +0,0 @@ -using exercise.pizzashopapi.Repository; -using Microsoft.AspNetCore.Mvc; - -namespace exercise.pizzashopapi.EndPoints -{ - public static class PizzaShopApi - { - public static void ConfigurePizzaShopApi(this WebApplication app) - { - - } - - - } -} diff --git a/exercise.pizzashopapi/Endpoints/CustomerEndpoints.cs b/exercise.pizzashopapi/Endpoints/CustomerEndpoints.cs new file mode 100644 index 0000000..22bcedb --- /dev/null +++ b/exercise.pizzashopapi/Endpoints/CustomerEndpoints.cs @@ -0,0 +1,94 @@ +using AutoMapper; +using exercise.pizzashopapi.DTO; +using exercise.pizzashopapi.Exceptions; +using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Repository; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace exercise.pizzashopapi.Endpoints +{ + public static class CustomerEndpoints + { + public static string Path { get; private set; } = "customers"; + public static void ConfigureCustomersEndpoints(this WebApplication app) + { + var group = app.MapGroup(Path); + + group.MapGet("/", GetCustomers); + group.MapPost("/", CreateCustomer); + group.MapGet("/{id}", GetCustomer); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetCustomers(IRepository repository, IMapper mapper) + { + try + { + IEnumerable customers = await repository.GetAll( + q => q.Include(x => x.Orders).ThenInclude(x => x.Toppings), + q => q.Include(x => x.Orders).ThenInclude(x => x.Product) + ); + return TypedResults.Ok(mapper.Map>(customers)); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetCustomer(IRepository repository, IMapper mapper, int id) + { + try + { + Customer customer = await repository.Get(id, + q => q.Include(x => x.Orders).ThenInclude(x => x.Toppings), + q => q.Include(x => x.Orders).ThenInclude(x => x.Product) + ); + return TypedResults.Ok(mapper.Map(customer)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task CreateCustomer( + IRepository repository, + IRepository customerRepository, + IRepository productRepository, + IMapper mapper, + CustomerPost entity) + { + try + { + Customer customer = await repository.Add(new Customer + { + Name = entity.Name + }); + customer = await customerRepository.Add(customer); + return TypedResults.Created($"{Path}/{customer.Id}", mapper.Map(customer)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + } +} diff --git a/exercise.pizzashopapi/Endpoints/OrderEndpoints.cs b/exercise.pizzashopapi/Endpoints/OrderEndpoints.cs new file mode 100644 index 0000000..4fcfd63 --- /dev/null +++ b/exercise.pizzashopapi/Endpoints/OrderEndpoints.cs @@ -0,0 +1,225 @@ +using AutoMapper; +using exercise.pizzashopapi.DTO; +using exercise.pizzashopapi.Enums; +using exercise.pizzashopapi.Exceptions; +using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Repository; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace exercise.pizzashopapi.Endpoints +{ + public static class OrderEndpoints + { + public static string Path { get; private set; } = "orders"; + public static void ConfigureOrdersEndpoints(this WebApplication app) + { + var group = app.MapGroup(Path); + + group.MapGet("/", GetOrders); + group.MapPost("/", CreateOrder); + group.MapGet("/{id}", GetOrder); + group.MapPost("/{id}/add-topping", AddTopping); + group.MapPost("/{id}/set-delivered", SetDelivered); + group.MapPost("/update-orders", UpdateOrders); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetOrders( + IRepository repository, + IMapper mapper, + string? customerId) + { + try + { + IEnumerable orders = await repository.GetAll( + q => q.Include(x => x.Customer), + q => q.Include(x => x.Product), + q => q.Include(x => x.Toppings) + ); + + if (!string.IsNullOrEmpty(customerId)) + { + int id; + if (!int.TryParse(customerId, out id)) return TypedResults.BadRequest("The customerId must be of type int!"); + orders = orders.Where(a => a.CustomerId == id); + } + + return TypedResults.Ok(mapper.Map>(orders)); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetOrder(IRepository repository, IMapper mapper, int id) + { + try + { + Order order = await repository.Get(id, + q => q.Include(x => x.Customer), + q => q.Include(x => x.Product), + q => q.Include(x => x.Toppings) + ); + return TypedResults.Ok(mapper.Map(order)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task CreateOrder( + IRepository repository, + IRepository customerRepository, + IRepository productRepository, + IMapper mapper, + OrderPost entity) + { + try + { + Customer customer = await customerRepository.Get(entity.CustomerId); + Product product = await productRepository.Get(entity.ProductId); + + Order order = await repository.Add(new Order + { + CustomerId = customer.Id, + Customer = customer, + ProductId = product.Id, + Product = product, + }); + return TypedResults.Created($"{Path}/{order.Id}", mapper.Map(order)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task AddTopping( + IRepository repository, + IRepository toppingRepository, + IMapper mapper, + int id, + OrderPostAddTopping entity) + { + try + { + Order order = await repository.Get(id, q => q.Include(x => x.Product).Include(x => x.Toppings)); + Topping topping = await toppingRepository.Get(entity.ToppingId); + + order.Toppings.Add(topping); + await repository.Update(order); + + return TypedResults.Created($"{Path}/{order.Id}", mapper.Map(order)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task SetDelivered( + IRepository repository, + IMapper mapper, + int id, + bool isDelivered) + { + try + { + Order order = await repository.Get(id, q => q.Include(x => x.Product).Include(x => x.Toppings).Include(x => x.Customer)); + + order.IsDelivered = isDelivered; + await repository.Update(order); + + return TypedResults.Ok(mapper.Map(order)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + public static async Task UpdateOrders( + IRepository repository, + IMapper mapper) + { + try + { + IEnumerable orders = await repository.FindAll( + condition: o => o.PreparationStage != PreparationStage.Finished, + orderBy: o => o.CreatedAt, + ascending: true, + q => q.Include(x => x.Customer).Include(x => x.Product).Include(x => x.Toppings) + ); + int cookingLimit = 4; // TODO: This and the following logic should be moved elsewhere. + int count = 0; + foreach (var order in orders.Where(o => o.PreparationStage != PreparationStage.Waiting)) + { + Tuple prepTime = ProductInfo.ProductPrepTimes[order.Product.ProductType]; + int totalPrepTime = prepTime.Item1 + prepTime.Item2; + decimal workTime = (decimal) (DateTime.UtcNow - order.StartedAt)!.Value.TotalMinutes; + if (workTime > totalPrepTime) + { + order.PreparationStage = PreparationStage.Finished; + order.CompletedAt = DateTime.UtcNow; + continue; + } else if (workTime > prepTime.Item1) + { + order.PreparationStage = PreparationStage.Cooking; + } + count++; + } + if (count < cookingLimit) + { + foreach (var order in orders.Where(o => o.PreparationStage == PreparationStage.Waiting).Take(cookingLimit - count)) + { + order.PreparationStage = PreparationStage.Preparing; + order.StartedAt = DateTime.UtcNow; + } + } + await repository.Save(); + return TypedResults.Ok(mapper.Map>(await repository.GetAll(q => q.Include(x => x.Customer).Include(x => x.Product).Include(x => x.Toppings)))); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + } +} diff --git a/exercise.pizzashopapi/Endpoints/ProductEnpoints.cs b/exercise.pizzashopapi/Endpoints/ProductEnpoints.cs new file mode 100644 index 0000000..4b01630 --- /dev/null +++ b/exercise.pizzashopapi/Endpoints/ProductEnpoints.cs @@ -0,0 +1,72 @@ +using AutoMapper; +using exercise.pizzashopapi.DTO; +using exercise.pizzashopapi.Enums; +using exercise.pizzashopapi.Exceptions; +using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Repository; +using Microsoft.AspNetCore.Mvc; + +namespace exercise.pizzashopapi.EndPoints +{ + public static class ProductEnpoints + { + public static string Path { get; private set; } = "products"; + public static void ConfigureProductsEndpoints(this WebApplication app) + { + var group = app.MapGroup(Path); + + group.MapGet("/", GetProducts); + group.MapGet("/{id}", GetProduct); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetProducts( + IRepository repository, + IMapper mapper, + string? productType) + { + try + { + IEnumerable products = await repository.GetAll(); + + if (!string.IsNullOrEmpty(productType)) + { + ProductType t; + if (!Enum.TryParse(productType, true, out t)) + { + return TypedResults.BadRequest($"That is not a valid appointment type! Choose one of {string.Join(", ", Enum.GetValues())}"); + } + products = products.Where(p => p.ProductType == t); + } + + return TypedResults.Ok(mapper.Map>(products)); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetProduct(IRepository repository, IMapper mapper, int id) + { + try + { + Product product = await repository.Get(id); + return TypedResults.Ok(mapper.Map(product)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + } +} diff --git a/exercise.pizzashopapi/Endpoints/ToppingEndpoints.cs b/exercise.pizzashopapi/Endpoints/ToppingEndpoints.cs new file mode 100644 index 0000000..ae97ce7 --- /dev/null +++ b/exercise.pizzashopapi/Endpoints/ToppingEndpoints.cs @@ -0,0 +1,60 @@ +using AutoMapper; +using exercise.pizzashopapi.DTO; +using exercise.pizzashopapi.Enums; +using exercise.pizzashopapi.Exceptions; +using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Repository; +using Microsoft.AspNetCore.Mvc; + +namespace exercise.pizzashopapi.EndPoints +{ + public static class ToppingEnpoints + { + public static string Path { get; private set; } = "toppings"; + public static void ConfigureToppingsEndpoints(this WebApplication app) + { + var group = app.MapGroup(Path); + + group.MapGet("/", GetToppings); + group.MapGet("/{id}", GetTopping); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetToppings( + IRepository repository, + IMapper mapper) + { + try + { + IEnumerable toppings = await repository.GetAll(); + return TypedResults.Ok(mapper.Map>(toppings)); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetTopping(IRepository repository, IMapper mapper, int id) + { + try + { + Topping topping = await repository.Get(id); + return TypedResults.Ok(mapper.Map(topping)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + } +} diff --git a/exercise.pizzashopapi/Enums/PreparationStage.cs b/exercise.pizzashopapi/Enums/PreparationStage.cs new file mode 100644 index 0000000..95699cb --- /dev/null +++ b/exercise.pizzashopapi/Enums/PreparationStage.cs @@ -0,0 +1,10 @@ +namespace exercise.pizzashopapi.Enums +{ + public enum PreparationStage + { + Waiting, + Preparing, + Cooking, + Finished + } +} diff --git a/exercise.pizzashopapi/Enums/ProductType.cs b/exercise.pizzashopapi/Enums/ProductType.cs new file mode 100644 index 0000000..4ae3059 --- /dev/null +++ b/exercise.pizzashopapi/Enums/ProductType.cs @@ -0,0 +1,22 @@ +namespace exercise.pizzashopapi.Enums +{ + public enum ProductType + { + Pizza, + Burger, + Fries, + Drinks + } + + public static class ProductInfo + { + public static Dictionary> ProductPrepTimes = new Dictionary> + { + { ProductType.Pizza, new Tuple(3, 12) }, + { ProductType.Burger, new Tuple(2, 10) }, + { ProductType.Fries, new Tuple(0, 10) }, + { ProductType.Drinks, new Tuple(1, 0) }, + }; + + } +} diff --git a/exercise.pizzashopapi/Exceptions/IdNotFoundException.cs b/exercise.pizzashopapi/Exceptions/IdNotFoundException.cs new file mode 100644 index 0000000..aa7fd9d --- /dev/null +++ b/exercise.pizzashopapi/Exceptions/IdNotFoundException.cs @@ -0,0 +1,11 @@ +namespace exercise.pizzashopapi.Exceptions +{ + public class IdNotFoundException : ArgumentException + { + public IdNotFoundException() { } + + public IdNotFoundException(string? message) : base(message) { } + + public IdNotFoundException(string? message, Exception? innerException) : base(message, innerException) { } + } +} diff --git a/exercise.pizzashopapi/Models/Customer.cs b/exercise.pizzashopapi/Models/Customer.cs index 2ca83bd..521d59a 100644 --- a/exercise.pizzashopapi/Models/Customer.cs +++ b/exercise.pizzashopapi/Models/Customer.cs @@ -6,5 +6,7 @@ public class Customer { public int Id { get; set; } public string Name { get; set; } + + public List Orders { get; set; } = []; } } diff --git a/exercise.pizzashopapi/Models/Order.cs b/exercise.pizzashopapi/Models/Order.cs index fbe6113..ffdc9c0 100644 --- a/exercise.pizzashopapi/Models/Order.cs +++ b/exercise.pizzashopapi/Models/Order.cs @@ -1,10 +1,24 @@ using System.ComponentModel.DataAnnotations.Schema; +using exercise.pizzashopapi.Enums; namespace exercise.pizzashopapi.Models { public class Order { - - + public int Id { get; set; } + public bool IsDelivered { get; set; } = false; + public PreparationStage PreparationStage { get; set; } = PreparationStage.Waiting; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? StartedAt { get; set; } = null; + public DateTime? CompletedAt { get; set; } = null; + + public int ProductId { get; set; } + public Product Product { get; set; } + public int CustomerId { get; set; } + public Customer Customer { get; set; } + public List Toppings { get; set; } = []; + + [NotMapped] + public decimal TotalPrice => Product.Price + Toppings.Sum(t => t.Price); } } diff --git a/exercise.pizzashopapi/Models/OrderTopping.cs b/exercise.pizzashopapi/Models/OrderTopping.cs new file mode 100644 index 0000000..0820bf7 --- /dev/null +++ b/exercise.pizzashopapi/Models/OrderTopping.cs @@ -0,0 +1,8 @@ +namespace exercise.pizzashopapi.Models +{ + public class OrderTopping + { + public int OrderId { get; set; } + public int ToppingId { get; set; } + } +} diff --git a/exercise.pizzashopapi/Models/Pizza.cs b/exercise.pizzashopapi/Models/Product.cs similarity index 58% rename from exercise.pizzashopapi/Models/Pizza.cs rename to exercise.pizzashopapi/Models/Product.cs index 5c085ec..cbcc9ca 100644 --- a/exercise.pizzashopapi/Models/Pizza.cs +++ b/exercise.pizzashopapi/Models/Product.cs @@ -1,12 +1,15 @@ using System.ComponentModel.DataAnnotations.Schema; +using exercise.pizzashopapi.Enums; namespace exercise.pizzashopapi.Models { - - public class Pizza + public class Product { public int Id { get; set; } + public ProductType ProductType { get; set; } public string Name { get; set; } public decimal Price { get; set; } + + public List Orders { get; set; } = []; } } \ No newline at end of file diff --git a/exercise.pizzashopapi/Models/Topping.cs b/exercise.pizzashopapi/Models/Topping.cs new file mode 100644 index 0000000..5940fda --- /dev/null +++ b/exercise.pizzashopapi/Models/Topping.cs @@ -0,0 +1,11 @@ +namespace exercise.pizzashopapi.Models +{ + public class Topping + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + + public List Orders { get; set; } = []; + } +} diff --git a/exercise.pizzashopapi/Program.cs b/exercise.pizzashopapi/Program.cs index c04a440..69545e9 100644 --- a/exercise.pizzashopapi/Program.cs +++ b/exercise.pizzashopapi/Program.cs @@ -1,35 +1,51 @@ +using System.Numerics; using exercise.pizzashopapi.Data; using exercise.pizzashopapi.EndPoints; +using exercise.pizzashopapi.Models; using exercise.pizzashopapi.Repository; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore; +using Scalar.AspNetCore; +using exercise.pizzashopapi.Endpoints; var builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddControllers(); -builder.Services.AddScoped(); -builder.Services.AddDbContext(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + +builder.Services.AddOpenApi(); +builder.Services.AddDbContext(options => +{ + var connectionString = configuration.GetConnectionString("DefaultConnectionString"); + options.UseNpgsql(connectionString); + + options.ConfigureWarnings(warnings => + warnings.Ignore(RelationalEventId.PendingModelChangesWarning)); +}); + +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddAutoMapper(typeof(Program)); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(); + app.MapOpenApi(); + app.MapScalarApiReference(); } app.UseHttpsRedirection(); -app.UseAuthorization(); - -app.MapControllers(); - -app.ConfigurePizzaShopApi(); - -app.SeedPizzaShopApi(); +app.ConfigureProductsEndpoints(); +app.ConfigureCustomersEndpoints(); +app.ConfigureOrdersEndpoints(); +app.ConfigureToppingsEndpoints(); app.Run(); diff --git a/exercise.pizzashopapi/Properties/launchSettings.json b/exercise.pizzashopapi/Properties/launchSettings.json index 1d7269e..a0cadab 100644 --- a/exercise.pizzashopapi/Properties/launchSettings.json +++ b/exercise.pizzashopapi/Properties/launchSettings.json @@ -1,19 +1,11 @@ { "$schema": "https://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:39663", - "sslPort": 44368 - } - }, "profiles": { "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", + "launchBrowser": false, + "launchUrl": "scalar/v1", "applicationUrl": "http://localhost:5070", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -22,20 +14,12 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", + "launchBrowser": false, + "launchUrl": "scalar/v1", "applicationUrl": "https://localhost:7138;http://localhost:5070", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } } } } diff --git a/exercise.pizzashopapi/Repository/IRepository.cs b/exercise.pizzashopapi/Repository/IRepository.cs index b114ea8..515f15d 100644 --- a/exercise.pizzashopapi/Repository/IRepository.cs +++ b/exercise.pizzashopapi/Repository/IRepository.cs @@ -1,11 +1,26 @@ -using exercise.pizzashopapi.Models; +using System.Linq.Expressions; namespace exercise.pizzashopapi.Repository { - public interface IRepository + public interface IRepository + where T : class + where U : struct { - Task> GetOrdersByCustomer(int id); - + Task> GetAll(params Func, IQueryable>[] includeChains); + Task Get(U id, string idField, params Func, IQueryable>[] includeChains); + Task Get(U id, params Func, IQueryable>[] includeChains); + + + Task Add(T entity); + Task Update(T entity); + Task Delete(U id); + Task Save(); + Task Find(Expression> condition, params Func, IQueryable>[] includeChains); + Task> FindAll( + Expression>? condition = null, + Expression>? orderBy = null, + bool ascending = true, + params Func, IQueryable>[] includeChains); } } diff --git a/exercise.pizzashopapi/Repository/Repository.cs b/exercise.pizzashopapi/Repository/Repository.cs index e109fce..c4cfc24 100644 --- a/exercise.pizzashopapi/Repository/Repository.cs +++ b/exercise.pizzashopapi/Repository/Repository.cs @@ -1,14 +1,100 @@ using exercise.pizzashopapi.Data; -using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Exceptions; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; namespace exercise.pizzashopapi.Repository { - public class Repository : IRepository + public class Repository : IRepository + where T : class + where U : struct { private DataContext _db; - public Task> GetOrdersByCustomer(int id) + private DbSet _table = null!; + public Repository(DataContext db) { - throw new NotImplementedException(); + _db = db; + _table = _db.Set(); + } + + public async Task Add(T entity) + { + await _table.AddAsync(entity); + await _db.SaveChangesAsync(); + return entity; + } + + public async Task Delete(U id) + { + T entity = await Get(id); + _table.Remove(entity); + await _db.SaveChangesAsync(); + return entity; + } + + public async Task Get(U id, string idField, params Func, IQueryable>[] includeChains) + { + IQueryable query = GetIncludeTable(includeChains); + T? entity = await query.FirstOrDefaultAsync(e => EF.Property(e, idField).Equals(id)); + return entity ?? throw new IdNotFoundException($"That ID does not exist for {typeof(T).Name}"); + } + public async Task Get(U id, params Func, IQueryable>[] includeChains) + { + return await Get(id, "Id", includeChains); + } + + public async Task> GetAll(params Func, IQueryable>[] includeChains) + { + IQueryable query = GetIncludeTable(includeChains); + return await query.ToListAsync(); + } + + public async Task Save() + { + await _db.SaveChangesAsync(); + } + + public async Task Find(Expression> condition, params Func, IQueryable>[] includeChains) + { + IQueryable query = GetIncludeTable(includeChains); + T? entity = await query.FirstOrDefaultAsync(condition); + return entity ?? throw new IdNotFoundException($"That ID does not exist for {typeof(T).Name}"); + } + + public async Task Update(T entity) + { + _table.Attach(entity); + _db.Entry(entity).State = EntityState.Modified; + await _db.SaveChangesAsync(); + return entity; + } + + public async Task> FindAll( + Expression>? condition = null, + Expression>? orderBy = null, + bool ascending = true, + params Func, IQueryable>[] includeChains) + { + IQueryable query = GetIncludeTable(includeChains); + + if (condition != null) + { + query = query.Where(condition); + } + if (orderBy != null) + { + query = ascending ? query.OrderBy(orderBy) : query.OrderByDescending(orderBy); + } + return await query.ToListAsync(); + } + private IQueryable GetIncludeTable(params Func, IQueryable>[] includeChains) + { + IQueryable query = _table; + foreach (var includeChain in includeChains) + { + query = includeChain(query); + } + return query; } } } diff --git a/exercise.pizzashopapi/Tools/MappingProfile.cs b/exercise.pizzashopapi/Tools/MappingProfile.cs new file mode 100644 index 0000000..e72825e --- /dev/null +++ b/exercise.pizzashopapi/Tools/MappingProfile.cs @@ -0,0 +1,30 @@ +using AutoMapper; +using exercise.pizzashopapi.DTO; +using exercise.pizzashopapi.Models; + +namespace exercise.pizzashopapi.Tools +{ + public class MappingProfile : Profile + { + public MappingProfile() + { + CreateMap() + .ForMember(p => p.ProductType, opt => opt.MapFrom(src => src.ProductType.ToString())); + + CreateMap() + .ForMember(p => p.PreparationStage, opt => opt.MapFrom(src => src.PreparationStage.ToString())); + CreateMap() + .ForMember(p => p.PreparationStage, opt => opt.MapFrom(src => src.PreparationStage.ToString())); + CreateMap() + .ForMember(p => p.PreparationStage, opt => opt.MapFrom(src => src.PreparationStage.ToString())); + CreateMap() + .ForMember(p => p.PreparationStage, opt => opt.MapFrom(src => src.PreparationStage.ToString())); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + } + } +} diff --git a/exercise.pizzashopapi/exercise.pizzashopapi.csproj b/exercise.pizzashopapi/exercise.pizzashopapi.csproj index 624203b..b38f832 100644 --- a/exercise.pizzashopapi/exercise.pizzashopapi.csproj +++ b/exercise.pizzashopapi/exercise.pizzashopapi.csproj @@ -11,18 +11,19 @@ - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + +