From 6b84d473d9a623c19ad3c4e4b08331ec2f6fc57b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20W=C3=B3jcik?= <81220416+HubertWojcik10@users.noreply.github.com> Date: Thu, 2 Nov 2023 18:45:50 +0100 Subject: [PATCH 1/3] Create endpoints to add, update, and deactivate a product (#215) * create endpoints to add, update, and deactivate a product * add swagger xml examples to dto --- .../Services/v2/IProductService.cs | 4 + .../Services/v2/ProductService.cs | 78 +++++++++++++++++++ .../v2/Product/AddProductRequest.cs | 70 +++++++++++++++++ .../v2/Product/ChangedProductResponse.cs | 63 +++++++++++++++ .../v2/Product/UpdateProductRequest.cs | 73 +++++++++++++++++ .../Controllers/v2/ProductsController.cs | 50 +++++++++--- 6 files changed, 328 insertions(+), 10 deletions(-) create mode 100644 coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/AddProductRequest.cs create mode 100644 coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/ChangedProductResponse.cs create mode 100644 coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/UpdateProductRequest.cs diff --git a/coffeecard/CoffeeCard.Library/Services/v2/IProductService.cs b/coffeecard/CoffeeCard.Library/Services/v2/IProductService.cs index 2bea42cc..e9382074 100644 --- a/coffeecard/CoffeeCard.Library/Services/v2/IProductService.cs +++ b/coffeecard/CoffeeCard.Library/Services/v2/IProductService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using CoffeeCard.Models.DataTransferObjects.v2.Product; using CoffeeCard.Models.Entities; namespace CoffeeCard.Library.Services.v2 @@ -10,5 +11,8 @@ public interface IProductService : IDisposable Task> GetPublicProductsAsync(); Task> GetProductsForUserAsync(User user); Task GetProductAsync(int productId); + Task AddProduct(AddProductRequest product); + + Task UpdateProduct(UpdateProductRequest product); } } \ No newline at end of file diff --git a/coffeecard/CoffeeCard.Library/Services/v2/ProductService.cs b/coffeecard/CoffeeCard.Library/Services/v2/ProductService.cs index 14f6ac68..666cd4ba 100644 --- a/coffeecard/CoffeeCard.Library/Services/v2/ProductService.cs +++ b/coffeecard/CoffeeCard.Library/Services/v2/ProductService.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; +using System.Data; using System.Linq; using System.Threading.Tasks; using CoffeeCard.Common.Errors; using CoffeeCard.Library.Persistence; +using CoffeeCard.Models.DataTransferObjects.v2.Product; using CoffeeCard.Models.Entities; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; @@ -53,6 +55,82 @@ public async Task GetProductAsync(int productId) return product; } + private async Task CheckProductUniquenessAsync(string name, int price) + { + var product = await _context.Products + .FirstOrDefaultAsync(p => (p.Name == name && p.Price == price)); + + return product == null; + } + + public async Task AddProduct(AddProductRequest newProduct) + { + var unique = await CheckProductUniquenessAsync(newProduct.Name, newProduct.Price); + if (!unique) + { + throw new ConflictException($"Product already exists with name {newProduct.Name} and price of {newProduct.Price}"); + } + + var product = new Product() + { + Price = newProduct.Price, + Description = newProduct.Description, + Name = newProduct.Name, + NumberOfTickets = newProduct.NumberOfTickets, + ExperienceWorth = 0, + Visible = newProduct.Visible + }; + + _context.Products.Add(product); + await _context.SaveChangesAsync(); + + var productUserGroups = newProduct.AllowedUserGroups.Select(userGroup => new ProductUserGroup + { + ProductId = product.Id, + UserGroup = userGroup + }).ToList(); + + _context.ProductUserGroups.AddRange(productUserGroups); + + + await _context.SaveChangesAsync(); + + + var result = new ChangedProductResponse + { + Price = product.Price, + Description = product.Description, + Name = product.Name, + NumberOfTickets = product.NumberOfTickets, + Visible = product.Visible + }; + + return result; + } + + public async Task UpdateProduct(UpdateProductRequest changedProduct) + { + var product = await GetProductAsync(changedProduct.Id); + product.Price = changedProduct.Price; + product.Description = changedProduct.Description; + product.NumberOfTickets = changedProduct.NumberOfTickets; + product.Name = changedProduct.Name; + product.Visible = changedProduct.Visible; + + await _context.SaveChangesAsync(); + + var result = new ChangedProductResponse + { + Price = product.Price, + Description = product.Description, + Name = product.Name, + NumberOfTickets = product.NumberOfTickets, + Visible = product.Visible + }; + + return result; + } + public void Dispose() { _context?.Dispose(); diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/AddProductRequest.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/AddProductRequest.cs new file mode 100644 index 00000000..6c42d988 --- /dev/null +++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/AddProductRequest.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using CoffeeCard.Models.Entities; + +namespace CoffeeCard.Models.DataTransferObjects.v2.Product +{ + /// + /// Initiate a new product add request. + /// + /// + /// { + /// "Name": "Latte", + /// "Price": 25, + /// "NumberOfTickets": 10, + /// "Description": "xxx", + /// "Visible": true + /// } + /// + public class AddProductRequest + { + /// + /// Gets or sets the price of the product. + /// + /// Product Price + /// 10 + [Required] + [Range(0, int.MaxValue, ErrorMessage = "Price must be a non-negative integer.")] + public int Price { get; set; } + + /// + /// Gets or sets the number of tickets associated with the product. + /// + /// Number of tickets associated with a product + /// 5 + [Required] + [Range(0, int.MaxValue, ErrorMessage = "Number of Tickets must be a non-negative integer.")] + public int NumberOfTickets { get; set; } + + /// + /// Gets or sets the name of the product. + /// + /// Product Name + /// Latte + [Required] + [MinLength(1, ErrorMessage = "Name cannot be an empty string.")] + public string Name { get; set; } + + /// + /// Gets or sets the description of the product. + /// + /// Product Description + /// A homemade latte with soy milk + [Required] + [MinLength(1, ErrorMessage = "Description cannot be an empty string.")] + public string Description { get; set; } + + /// + /// Gets or sets the visibility of the product. Default is true. + /// + /// Product Visibility + /// true + [DefaultValue(true)] + public bool Visible { get; set; } = true; + + [Required] + public IEnumerable AllowedUserGroups { get; set; } + + } +} \ No newline at end of file diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/ChangedProductResponse.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/ChangedProductResponse.cs new file mode 100644 index 00000000..9c9160bf --- /dev/null +++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/ChangedProductResponse.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace CoffeeCard.Models.DataTransferObjects.v2.Product +{ + /// + /// Represents the product response. + /// + public class ChangedProductResponse + { + /// + /// Gets or sets the price of the product. + /// + /// + /// { + /// "Price": 150, + /// "NumberOfTickets": 10, + /// "Name": "Espresso", + /// "Description": "A coffee made by forcing steam through ground coffee beans.", + /// "Visible": false + /// } + /// + [Required] + [Range(0, int.MaxValue, ErrorMessage = "Price must be a non-negative integer.")] + public int Price { get; set; } + + /// + /// Gets or sets the number of tickets associated with the product. + /// + /// Number of Tickets of a Product + /// 5 + [Required] + [Range(0, int.MaxValue, ErrorMessage = "Number of tickets must be a non-negative integer.")] + public int NumberOfTickets { get; set; } + + /// + /// Gets or sets the name of the product. + /// + /// Product Name + /// Espresso + [Required] + [MinLength(1, ErrorMessage = "Name cannot be an empty string.")] + public string Name { get; set; } + + /// + /// Gets or sets the description of the product. + /// + /// Product Description + /// A homemade espresso from fresh beans + [Required] + [MinLength(1, ErrorMessage = "Description cannot be an empty string.")] + public string Description { get; set; } + + /// + /// Gets or sets the visibility of the product. + /// + /// Product Visibility + /// true + [Required] + public bool Visible { get; set; } + } +} \ No newline at end of file diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/UpdateProductRequest.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/UpdateProductRequest.cs new file mode 100644 index 00000000..a939c1ae --- /dev/null +++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/UpdateProductRequest.cs @@ -0,0 +1,73 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace CoffeeCard.Models.DataTransferObjects.v2.Product +{ + /// /// + /// Initiate an update product request. + /// + /// + /// { + /// "Id": 1, + /// "Price": 150, + /// "NumberOfTickets": 10, + /// "Name": "Espresso", + /// "Description": "A coffee made by forcing steam through ground coffee beans.", + /// "Visible": false + /// } + /// + public class UpdateProductRequest + { + /// + /// Gets or sets the ID of the product to update. + /// + /// Product Id + /// 1 + [Required] + public int Id { get; set; } + + /// + /// Gets or sets the updated price of the product. + /// + /// Product Price + /// 10 + [Required] + [Range(0, int.MaxValue, ErrorMessage = "Price must be a non-negative integer.")] + public int Price { get; set; } + + /// + /// Gets or sets the updated number of tickets associated with the product. + /// + /// Number of Tickets of a Product + /// 5 + [Required] + [Range(0, int.MaxValue, ErrorMessage = "Number of Tickets must be a non-negative integer.")] + public int NumberOfTickets { get; set; } + + /// + /// Gets or sets the updated name of the product. + /// + /// Product Name + /// Espresso + [Required] + [MinLength(1, ErrorMessage = "Name cannot be an empty string.")] + public string Name { get; set; } + + /// + /// Gets or sets the updated description of the product. + /// + /// Product Description + /// A homemade espresso from fresh beans + [Required] + [MinLength(1, ErrorMessage = "Description cannot be an empty string.")] + public string Description { get; set; } + + /// + /// Gets or sets the updated visibility of the product. Default is true. + /// + /// Product Visibility + /// true + [DefaultValue(true)] + public bool Visible { get; set; } = true; + } +} \ No newline at end of file diff --git a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs index 02ccad09..c791a097 100644 --- a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs +++ b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs @@ -1,40 +1,70 @@ -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using CoffeeCard.Common.Errors; -using CoffeeCard.Library.Services.v2; using CoffeeCard.Library.Utils; +using CoffeeCard.Models.DataTransferObjects.v2.Product; using CoffeeCard.Models.DataTransferObjects.v2.Products; using CoffeeCard.Models.Entities; +using CoffeeCard.WebApi.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using IProductService = CoffeeCard.Library.Services.v2.IProductService; namespace CoffeeCard.WebApi.Controllers.v2 { /// - /// Controller for retrieving products + /// Controller for creating, changing, and deactivating a product /// + [ApiController] [Authorize] [ApiVersion("2")] - [Route("api/v{version:apiVersion}/[controller]")] - [ApiController] + [Route("api/v{version:apiVersion}/products")] + public class ProductsController : ControllerBase { - private readonly ClaimsUtilities _claimsUtilities; private readonly IProductService _productService; + private readonly ClaimsUtilities _claimsUtilities; /// /// Initializes a new instance of the class. /// - public ProductsController(IProductService productService, - ClaimsUtilities claimsUtilities) + public ProductsController(IProductService productService, ClaimsUtilities claimsUtilities) { _productService = productService; _claimsUtilities = claimsUtilities; } + /// + /// Adds a new product to the database. + /// + /// The request containing the details of the product to be added and allowed user groups. + /// The newly added product wrapped in a InitiateProductResponse object. + /// The request was successful, and the product was added. + [HttpPost("")] + [AuthorizeRoles(UserGroup.Board)] + [ProducesResponseType(typeof(ChangedProductResponse), StatusCodes.Status200OK)] + public async Task AddProduct(AddProductRequest addProductRequest) + { + return Ok(await _productService.AddProduct(addProductRequest)); + } + + + /// + /// Updates a product with the specified changes. + /// + /// The request containing the changes to be applied to the product. + /// A response indicating the result of the update operation. + /// The request was successful, and the product was updated. + [HttpPut("")] + [AuthorizeRoles(UserGroup.Board)] + [ProducesResponseType(typeof(ChangedProductResponse), StatusCodes.Status200OK)] + public async Task UpdateProduct(UpdateProductRequest product) + { + return Ok(await _productService.UpdateProduct(product)); + } + /// /// Returns a list of available products based on a account's user group /// @@ -42,8 +72,8 @@ public ProductsController(IProductService productService, /// Successful request [HttpGet] [AllowAnonymous] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public async Task>> GetProducts() + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetProducts() { IEnumerable products; try From 9c33d3b417401e278d71bbc89d9312ae820f2fdd Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Thu, 2 Nov 2023 19:15:56 +0100 Subject: [PATCH 2/3] fix: IsPerk (#219) --- coffeecard/CoffeeCard.Library/Utils/ProductExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coffeecard/CoffeeCard.Library/Utils/ProductExtensions.cs b/coffeecard/CoffeeCard.Library/Utils/ProductExtensions.cs index 64bfdda0..96c1f9f4 100644 --- a/coffeecard/CoffeeCard.Library/Utils/ProductExtensions.cs +++ b/coffeecard/CoffeeCard.Library/Utils/ProductExtensions.cs @@ -19,7 +19,7 @@ public static bool IsPerk(this Product product) throw new ArgumentNullException($"{nameof(Product.ProductUserGroup)} must not be null"); } - return product.ProductUserGroup.Any(pug => pug.UserGroup != UserGroup.Customer); + return product.ProductUserGroup.All(pug => pug.UserGroup != UserGroup.Customer); } } } From daed7ca939ed3e76129353fe7287565890455d76 Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Thu, 2 Nov 2023 19:23:45 +0100 Subject: [PATCH 3/3] Fixed the responsetype of getProducts (#220) --- .../CoffeeCard.WebApi/Controllers/v2/ProductsController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs index c791a097..36bed38f 100644 --- a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs +++ b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs @@ -72,8 +72,8 @@ public async Task UpdateProduct(UpdateProductRequest product) /// Successful request [HttpGet] [AllowAnonymous] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public async Task>> GetProducts() + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetProducts() { IEnumerable products; try @@ -105,4 +105,4 @@ private static ProductResponse MapProductToDto(Product product) }; } } -} \ No newline at end of file +}