diff --git a/coffeecard/CoffeeCard.Library/Services/v2/IProductService.cs b/coffeecard/CoffeeCard.Library/Services/v2/IProductService.cs index e9382074..8eb6c1ca 100644 --- a/coffeecard/CoffeeCard.Library/Services/v2/IProductService.cs +++ b/coffeecard/CoffeeCard.Library/Services/v2/IProductService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using CoffeeCard.Models.DataTransferObjects.v2.Product; +using CoffeeCard.Models.DataTransferObjects.v2.Products; using CoffeeCard.Models.Entities; namespace CoffeeCard.Library.Services.v2 @@ -11,8 +12,7 @@ public interface IProductService : IDisposable Task> GetPublicProductsAsync(); Task> GetProductsForUserAsync(User user); Task GetProductAsync(int productId); - Task AddProduct(AddProductRequest product); - - Task UpdateProduct(UpdateProductRequest product); + Task AddProduct(AddProductRequest product); + Task UpdateProduct(int productId, UpdateProductRequest updateProduct); } } \ 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 666cd4ba..cba8118a 100644 --- a/coffeecard/CoffeeCard.Library/Services/v2/ProductService.cs +++ b/coffeecard/CoffeeCard.Library/Services/v2/ProductService.cs @@ -63,7 +63,7 @@ private async Task CheckProductUniquenessAsync(string name, int price) return product == null; } - public async Task AddProduct(AddProductRequest newProduct) + public async Task AddProduct(AddProductRequest newProduct) { var unique = await CheckProductUniquenessAsync(newProduct.Name, newProduct.Price); if (!unique) @@ -91,13 +91,11 @@ public async Task AddProduct(AddProductRequest newProduc }).ToList(); _context.ProductUserGroups.AddRange(productUserGroups); - - await _context.SaveChangesAsync(); - - var result = new ChangedProductResponse + var result = new DetailedProductResponse { + Id = product.Id, Price = product.Price, Description = product.Description, Name = product.Name, @@ -108,19 +106,21 @@ public async Task AddProduct(AddProductRequest newProduc return result; } - public async Task UpdateProduct(UpdateProductRequest changedProduct) + public async Task UpdateProduct(int productId, UpdateProductRequest updateProduct) { - 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; + var product = await GetProductAsync(productId); + + product.Price = updateProduct.Price; + product.Description = updateProduct.Description; + product.NumberOfTickets = updateProduct.NumberOfTickets; + product.Name = updateProduct.Name; + product.Visible = updateProduct.Visible; await _context.SaveChangesAsync(); - var result = new ChangedProductResponse + var result = new DetailedProductResponse { + Id = product.Id, Price = product.Price, Description = product.Description, Name = product.Name, diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/AddProductRequest.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/AddProductRequest.cs index 6c42d988..b1ee35e3 100644 --- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/AddProductRequest.cs +++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/AddProductRequest.cs @@ -10,11 +10,12 @@ namespace CoffeeCard.Models.DataTransferObjects.v2.Product /// /// /// { - /// "Name": "Latte", - /// "Price": 25, - /// "NumberOfTickets": 10, - /// "Description": "xxx", - /// "Visible": true + /// "name": "Latte", + /// "price": 25, + /// "numberOfTickets": 10, + /// "description": "Milkbased espresso drink", + /// "visible": true, + /// "allowedUserGroups": ["Customer"] /// } /// public class AddProductRequest @@ -23,7 +24,7 @@ public class AddProductRequest /// Gets or sets the price of the product. /// /// Product Price - /// 10 + /// 10 [Required] [Range(0, int.MaxValue, ErrorMessage = "Price must be a non-negative integer.")] public int Price { get; set; } @@ -31,8 +32,8 @@ public class AddProductRequest /// /// Gets or sets the number of tickets associated with the product. /// - /// Number of tickets associated with a product - /// 5 + /// 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; } @@ -40,8 +41,8 @@ public class AddProductRequest /// /// Gets or sets the name of the product. /// - /// Product Name - /// Latte + /// Product Name + /// Latte [Required] [MinLength(1, ErrorMessage = "Name cannot be an empty string.")] public string Name { get; set; } @@ -49,8 +50,8 @@ public class AddProductRequest /// /// Gets or sets the description of the product. /// - /// Product Description - /// A homemade latte with soy milk + /// Product Description + /// A homemade latte with soy milk [Required] [MinLength(1, ErrorMessage = "Description cannot be an empty string.")] public string Description { get; set; } @@ -58,11 +59,16 @@ public class AddProductRequest /// /// Gets or sets the visibility of the product. Default is true. /// - /// Product Visibility - /// true + /// Product Visibility + /// true [DefaultValue(true)] public bool Visible { get; set; } = true; + + /// + /// List of UserGroups who are entitled to purchase the product + /// + /// ["Customer"] [Required] public IEnumerable AllowedUserGroups { get; set; } diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/ChangedProductResponse.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/DetailedProductResponse.cs similarity index 62% rename from coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/ChangedProductResponse.cs rename to coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/DetailedProductResponse.cs index 8a0cfba0..82acf39d 100644 --- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/ChangedProductResponse.cs +++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/DetailedProductResponse.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace CoffeeCard.Models.DataTransferObjects.v2.Product @@ -7,12 +5,30 @@ namespace CoffeeCard.Models.DataTransferObjects.v2.Product /// /// Represents the product response. /// - public class ChangedProductResponse + /// + /// { + /// "id": 1, + /// "price": 150, + /// "numberOfTickets": 10, + /// "name": "Espresso", + /// "description": "A homemade espresso from fresh beans", + /// "visible": true + /// } + /// + public class DetailedProductResponse { + /// + /// Product Id + /// + /// ProductId + /// 1 + [Required] + public int Id { get; set; } + /// /// Gets or sets the price of the product. /// - /// 150 + /// 150 [Required] [Range(0, int.MaxValue, ErrorMessage = "Price must be a non-negative integer.")] public int Price { get; set; } @@ -21,7 +37,7 @@ public class ChangedProductResponse /// Gets or sets the number of tickets associated with the product. /// /// Number of Tickets of a Product - /// 5 + /// 5 [Required] [Range(0, int.MaxValue, ErrorMessage = "Number of tickets must be a non-negative integer.")] public int NumberOfTickets { get; set; } @@ -29,8 +45,8 @@ public class ChangedProductResponse /// /// Gets or sets the name of the product. /// - /// Product Name - /// Espresso + /// Product Name + /// Espresso [Required] [MinLength(1, ErrorMessage = "Name cannot be an empty string.")] public string Name { get; set; } @@ -38,8 +54,8 @@ public class ChangedProductResponse /// /// Gets or sets the description of the product. /// - /// Product Description - /// A homemade espresso from fresh beans + /// Product Description + /// A homemade espresso from fresh beans [Required] [MinLength(1, ErrorMessage = "Description cannot be an empty string.")] public string Description { get; set; } @@ -47,8 +63,8 @@ public class ChangedProductResponse /// /// Gets or sets the visibility of the product. /// - /// Product Visibility - /// true + /// Product Visibility + /// true [Required] public bool Visible { get; set; } } diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/UpdateProductRequest.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/UpdateProductRequest.cs index a939c1ae..a9171341 100644 --- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/UpdateProductRequest.cs +++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Product/UpdateProductRequest.cs @@ -8,29 +8,20 @@ namespace CoffeeCard.Models.DataTransferObjects.v2.Product /// /// /// { - /// "Id": 1, - /// "Price": 150, - /// "NumberOfTickets": 10, - /// "Name": "Espresso", - /// "Description": "A coffee made by forcing steam through ground coffee beans.", - /// "Visible": false + /// "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 + /// Product Price + /// 10 [Required] [Range(0, int.MaxValue, ErrorMessage = "Price must be a non-negative integer.")] public int Price { get; set; } @@ -38,8 +29,8 @@ public class UpdateProductRequest /// /// Gets or sets the updated number of tickets associated with the product. /// - /// Number of Tickets of a Product - /// 5 + /// 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; } @@ -47,8 +38,8 @@ public class UpdateProductRequest /// /// Gets or sets the updated name of the product. /// - /// Product Name - /// Espresso + /// Product Name + /// Espresso [Required] [MinLength(1, ErrorMessage = "Name cannot be an empty string.")] public string Name { get; set; } @@ -56,8 +47,8 @@ public class UpdateProductRequest /// /// Gets or sets the updated description of the product. /// - /// Product Description - /// A homemade espresso from fresh beans + /// Product Description + /// A homemade espresso from fresh beans [Required] [MinLength(1, ErrorMessage = "Description cannot be an empty string.")] public string Description { get; set; } @@ -65,8 +56,8 @@ public class UpdateProductRequest /// /// Gets or sets the updated visibility of the product. Default is true. /// - /// Product Visibility - /// true + /// Product Visibility + /// true [DefaultValue(true)] public bool Visible { get; set; } = true; } diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Products/ProductResponse.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Products/SimpleProductResponse.cs similarity index 97% rename from coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Products/ProductResponse.cs rename to coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Products/SimpleProductResponse.cs index b9dfad28..a8cbab5b 100644 --- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Products/ProductResponse.cs +++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Products/SimpleProductResponse.cs @@ -15,7 +15,7 @@ namespace CoffeeCard.Models.DataTransferObjects.v2.Products /// "isPerk": true /// } /// - public class ProductResponse + public class SimpleProductResponse { /// /// Id of product diff --git a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs index 36bed38f..7bb4eafc 100644 --- a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs +++ b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ProductsController.cs @@ -41,28 +41,49 @@ public ProductsController(IProductService productService, ClaimsUtilities claims /// /// 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("")] + /// Product created + /// Product name already exists + [HttpPost] [AuthorizeRoles(UserGroup.Board)] - [ProducesResponseType(typeof(ChangedProductResponse), StatusCodes.Status200OK)] - public async Task AddProduct(AddProductRequest addProductRequest) + [ProducesResponseType(typeof(DetailedProductResponse), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ApiError), StatusCodes.Status409Conflict)] + public async Task> AddProduct([FromBody] AddProductRequest addProductRequest) { - return Ok(await _productService.AddProduct(addProductRequest)); - } + var product = await _productService.AddProduct(addProductRequest); + return CreatedAtAction(nameof(GetProductById), new { id = product.Id, version = 2 }, product); + } /// /// Updates a product with the specified changes. /// + /// Product Id /// 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("")] + /// Product updated + /// Product not found + [HttpPut("{id:int}")] [AuthorizeRoles(UserGroup.Board)] - [ProducesResponseType(typeof(ChangedProductResponse), StatusCodes.Status200OK)] - public async Task UpdateProduct(UpdateProductRequest product) + [ProducesResponseType(typeof(DetailedProductResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)] + public async Task> UpdateProduct(int id, [FromBody] UpdateProductRequest product) + { + return Ok(await _productService.UpdateProduct(id, product)); + } + + /// + /// Get Product by Id + /// + /// Product Id + /// Detailed Product + /// Detailed Product + /// Product not found + [HttpGet("{id:int}")] + [ProducesResponseType(typeof(DetailedProductResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)] + public async Task> GetProductById(int id) { - return Ok(await _productService.UpdateProduct(product)); + return Ok(await _productService.GetProductAsync(id)); } /// @@ -72,8 +93,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 @@ -92,9 +113,9 @@ public async Task>> GetProducts() return Ok(products.Select(MapProductToDto).ToList()); } - private static ProductResponse MapProductToDto(Product product) + private static SimpleProductResponse MapProductToDto(Product product) { - return new ProductResponse + return new SimpleProductResponse { Id = product.Id, Name = product.Name, diff --git a/coffeecard/CoffeeCard.WebApi/appsettings.json b/coffeecard/CoffeeCard.WebApi/appsettings.json index d65081e1..a5aaf876 100644 --- a/coffeecard/CoffeeCard.WebApi/appsettings.json +++ b/coffeecard/CoffeeCard.WebApi/appsettings.json @@ -9,7 +9,7 @@ "DeploymentUrl": "https://localhost:8080/" }, "DatabaseSettings": { - "ConnectionString": "Server=localhost;Initial Catalog=master;User=sa;Password=Your_password123;TrustServerCertificate=True;", + "ConnectionString": "Server=mssql;Initial Catalog=master;User=sa;Password=Your_password123;TrustServerCertificate=True;", "SchemaName": "dbo" }, "IdentitySettings": {