diff --git a/EcommerceAPI.Ledana/.gitattributes b/EcommerceAPI.Ledana/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/EcommerceAPI.Ledana/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/EcommerceAPI.Ledana/.gitignore b/EcommerceAPI.Ledana/.gitignore new file mode 100644 index 00000000..9491a2fd --- /dev/null +++ b/EcommerceAPI.Ledana/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/Clients/CategoryApiClient.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Clients/CategoryApiClient.cs new file mode 100644 index 00000000..3d3b5e4d --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Clients/CategoryApiClient.cs @@ -0,0 +1,96 @@ +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Models; +using System.Net.Http.Json; + +namespace ECommerceUI.Ledana.Clients +{ + internal class CategoryApiClient + { + private static readonly HttpClient _client = new(); + + internal async Task CreateCategory(CategoryDto categoryDto) + { + try + { + var response = await _client.PostAsJsonAsync("https://localhost:7077/api/category", categoryDto); + + if (response.IsSuccessStatusCode) + return "Category was added successfully"; + + return "Creating category went wrong"; + } + catch (Exception e) + { + return "Creating category went wrong " + e.Message; + } + } + + internal async Task DeleteCategory(int categoryId) + { + try + { + var response = await _client.DeleteAsync($"https://localhost:7077/api/category/{categoryId}"); + + if (response.IsSuccessStatusCode) + return "Category was deleted successfully"; + + return "Deleting category went wrong"; + } + catch (Exception e) + { + return "Deleting category went wrong " + e.Message; + } + } + + internal async Task?> GetCategories() + { + try + { + var response = await _client.GetFromJsonAsync>>("https://localhost:7077/api/category"); + if (response is null) return null; + + return response.Data; + + } + catch (Exception e) + { + Console.WriteLine("Getting categories didn't work " + e.Message); + return null; + } + } + + internal async Task GetCategoryById(int categoryId) + { + try + { + var response = await _client.GetFromJsonAsync>($"https://localhost:7077/api/category/{categoryId}"); + if (response is null) return null; + + return response.Data; + + } + catch (Exception e) + { + Console.WriteLine("Getting category didn't work " + e.Message); + return null; + } + } + + internal async Task UpdateCategory(int categoryId, CategoryDto categoryDto) + { + try + { + var response = await _client.PutAsJsonAsync($"https://localhost:7077/api/category/{categoryId}", categoryDto); + + if (response.IsSuccessStatusCode) + return "Category was updated successfully"; + + return "Updating category went wrong"; + } + catch (Exception e) + { + return "Updating category went wrong " + e.Message; + } + } + } +} diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/Clients/ProductApiClient.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Clients/ProductApiClient.cs new file mode 100644 index 00000000..0c28ef28 --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Clients/ProductApiClient.cs @@ -0,0 +1,203 @@ +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Models; +using System.Net.Http.Json; + +namespace ECommerceUI.Ledana.Clients +{ + internal class ProductApiClient + { + private static readonly HttpClient _client = new(); + internal async Task CreateProduct(ProductDto product) + { + try + { + var response = await _client.PostAsJsonAsync("https://localhost:7077/api/product", product); + var result = await response.Content.ReadFromJsonAsync>(); + + if (response is null) return "Creating product didn't work "; + + if (response.IsSuccessStatusCode) + return "Product added succesfully"; + + return "Creating product didn't work "; + } + catch (Exception e) + { + return "Creating product didn't work " + e.Message; + } + } + + internal async Task DeleteProduct(int id) + { + try + { + var response = await _client.DeleteFromJsonAsync>($"https://localhost:7077/api/product/{id}"); + + if (response is null) return "Deleting product didn't work"; + + if (!response.RequestFailed) + return "Product deleted successfully!"; + + return response.ErrorMessage; + } + catch (Exception e) + { + return "Deleting product didn't work " + e.Message; + } + } + + internal async Task GetProductById(int id) + { + try + { + var response = await _client.GetFromJsonAsync>($"https://localhost:7077/api/product/{id}"); + + if (response is null) return null; + return response.Data; + + } + catch (Exception e) + { + Console.WriteLine("Getting product went wrong " + e.Message); + return null; + } + } + + internal async Task>?> GetProducts(int pageNumber, int pageSize) + { + try + { + + return await _client.GetFromJsonAsync>>($"https://localhost:7077/api/product?page_size={pageSize}&page_number={pageNumber}"); + } + catch (Exception e) + { + Console.WriteLine("Getting products went wrong " + e.Message); + return null; + } + } + internal async Task>?> GetProducts() + { + try + { + + return await _client.GetFromJsonAsync>>($"https://localhost:7077/api/product"); + } + catch (Exception e) + { + Console.WriteLine("Getting products went wrong " + e.Message); + return null; + } + } + //without pagination + internal async Task?> GetProductsWithoutPagination() + { + try + { + var response = await _client.GetFromJsonAsync>>($"https://localhost:7077/api/product/all"); + if (response is null) return null; + + return response.Data; + } + catch (Exception e) + { + Console.WriteLine("Getting products went wrong " + e.Message); + return null; + } + } + + + internal async Task UpdateProduct(int id, ProductUpdateDto product) + { + try + { + var response = await _client.PutAsJsonAsync($"https://localhost:7077/api/product/{id}", product); + + if (response.IsSuccessStatusCode) + return "Product updated successfully!"; + + return "Updating product didn't work"; + } + catch (Exception e) + { + return "Updating product didn't work " + e.Message; + } + } + internal async Task>?> GetProductsOrderedByPrice(int pageNumber, int pageSize) + { + try + { + return await _client.GetFromJsonAsync>>($"https://localhost:7077/api/product?page_size={pageSize}&page_number={pageNumber}&sort_by=price"); + } + catch (Exception e) + { + Console.WriteLine("Getting products went wrong " + e.Message); + return null; + } + } + + internal async Task>?> GetProductsOrderedByStock(int pageNumber, int pageSize) + { + try + { + return await _client.GetFromJsonAsync>>($"https://localhost:7077/api/product?page_size={pageSize}&page_number={pageNumber}&sort_by=stock"); + } + catch (Exception e) + { + Console.WriteLine("Getting products went wrong " + e.Message); + return null; + } + } + + internal async Task?> GetProductsWithName(string name) + { + try + { + var response = await _client.GetFromJsonAsync>>($"https://localhost:7077/api/product?name={name.Trim()}"); + + if (response is null) return null; + return response.Data; + + } + catch (Exception e) + { + Console.WriteLine("Getting product went wrong " + e.Message); + return null; + } + } + + internal async Task?> GetProductsCheaperThenPrice(decimal price) + { + try + { + var response = await _client.GetFromJsonAsync>>($"https://localhost:7077/api/product?price={price}"); + + if (response is null) return null; + return response.Data; + + } + catch (Exception e) + { + Console.WriteLine("Getting product went wrong " + e.Message); + return null; + } + } + + internal async Task?> ViewProductsLowerThenStock(int stock) + { + try + { + var response = await _client.GetFromJsonAsync>>($"https://localhost:7077/api/product?stock={stock}"); + + if (response is null) return null; + return response.Data; + + } + catch (Exception e) + { + Console.WriteLine("Getting product went wrong " + e.Message); + return null; + } + } + } +} diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/Clients/SaleApiClient.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Clients/SaleApiClient.cs new file mode 100644 index 00000000..413059cd --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Clients/SaleApiClient.cs @@ -0,0 +1,119 @@ +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Models; +using System.Globalization; +using System.Net.Http.Json; + +namespace ECommerceUI.Ledana.Clients +{ + internal class SaleApiClient + { + HttpClient httpClient = new(); + + internal async Task CreateSale(SaleDto saleDto) + { + try + { + var response = await httpClient.PostAsJsonAsync("https://localhost:7077/api/sale", saleDto); + + var content = await response.Content.ReadFromJsonAsync>(); + if (content is null) return "Creating sale went wrong!"; + + return "Sale added successfully!"; + } + catch (Exception e) + { + return "Creating sale went wrong!" + e.Message; + } + } + + internal async Task GetSaleById(int categoryId) + { + try + { + var response = await httpClient.GetFromJsonAsync>($"https://localhost:7077/api/sale/{categoryId}"); + if (response is null) return null; + + return response.Data; + } + catch (Exception e) + { + Console.WriteLine("Getting sales went wrong " + e.Message); + return null; + } + } + + internal async Task?> GetSalesCheaperThenPrice(decimal price) + { + try + { + var response = await httpClient.GetFromJsonAsync>>($"https://localhost:7077/api/sale?total_price={price}"); + if (response is null) return null; + + return response.Data; + } + catch (Exception e) + { + Console.WriteLine("Getting sales went wrong " + e.Message); + return null; + } + } + + internal async Task>?> GetSalesSortedByDate(int pageNumber, int pageSize) + { + try + { + return await httpClient.GetFromJsonAsync>>($"https://localhost:7077/api/sale?sort_by=date&page_size={pageSize}&page_number={pageNumber}"); + } + catch (Exception e) + { + Console.WriteLine("Getting sales went wrong " + e.Message); + return null; + } + } + + internal async Task>?> GetSalesSortedByTotalPrice(int pageNumber, int pageSize) + { + try + { + return await httpClient.GetFromJsonAsync>>($"https://localhost:7077/api/sale?sort_by=total_price&page_size={pageSize}&page_number={pageNumber}"); + } + catch (Exception e) + { + Console.WriteLine("Getting sales went wrong " + e.Message); + return null; + } + } + + internal async Task?> GetSalesWithCategoryName(string name) + { + try + { + var response = await httpClient.GetFromJsonAsync>>($"https://localhost:7077/api/sale?category_name={name.Trim()}"); + if (response is null) return null; + + return response.Data; + } + catch (Exception e) + { + Console.WriteLine("Getting sales went wrong " + e.Message); + return null; + } + } + + internal async Task?> GetSalesWithProductName(string name) + { + try + { + var response = await httpClient.GetFromJsonAsync>>($"https://localhost:7077/api/sale?product_name={name.Trim()}"); + if (response is null) return null; + + return response.Data; + } + catch (Exception e) + { + Console.WriteLine("Getting sales went wrong " + e.Message); + return null; + } + } + } +} diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/Controllers/CategoryUIController.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Controllers/CategoryUIController.cs new file mode 100644 index 00000000..c8955a5a --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Controllers/CategoryUIController.cs @@ -0,0 +1,68 @@ +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Models; +using ECommerceUI.Ledana.Clients; +using ECommerceUI.Ledana.Services; +using ECommerceUI.Ledana.UI; + +namespace ECommerceUI.Ledana.Controllers +{ + internal class CategoryUIController + { + static CategoryApiClient categoryApiClient = new(); + internal static async Task AddNewCategory() + { + string categoryName = CategoryUIService.GetCategoryName("Please put the name of the new category"); + CategoryDto categoryDto = new() + { + Name = categoryName + }; + Console.WriteLine(await categoryApiClient.CreateCategory(categoryDto)); + } + + internal static async Task DeleteCategory() + { + await ViewCategories(); + int categoryId = await CategoryUIService.GetCategoryId("Please put the id of the category you want to delete"); + if (categoryId == 0) return; + + Console.WriteLine(await categoryApiClient.DeleteCategory(categoryId)); + } + + internal static async Task UpdateCategory() + { + await ViewCategories(); + int categoryId = await CategoryUIService.GetCategoryId("Please put the id of the category you want to update"); + if (categoryId == 0) return; + + string categoryName = CategoryUIService.GetCategoryName("Please put the name of the new category"); + CategoryDto categoryDto = new() + { + Name = categoryName + }; + Console.WriteLine(await categoryApiClient.UpdateCategory(categoryId, categoryDto)); + } + + internal static async Task ViewCategories() + { + var categories = await categoryApiClient.GetCategories(); + if (categories is null || categories.Count == 0) Console.WriteLine("No categories found!"); + else + TableVisualisation.ShowCategories(categories); + } + + internal static async Task ViewCategoryById() + { + await ViewCategories(); + int categoryId = await CategoryUIService.GetCategoryId("Please put the id of the category you want to view"); + if (categoryId == 0) return; + + var category = await categoryApiClient.GetCategoryById(categoryId); + if (category is null) + { + Console.WriteLine("Category not found"); + return; + } + TableVisualisation.ShowCategory(category); + } + } +} diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/Controllers/ProductUIController.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Controllers/ProductUIController.cs new file mode 100644 index 00000000..0f84345e --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Controllers/ProductUIController.cs @@ -0,0 +1,287 @@ +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Models; +using ECommerceUI.Ledana.Clients; +using ECommerceUI.Ledana.Services; +using ECommerceUI.Ledana.UI; +using Spectre.Console; + +namespace ECommerceUI.Ledana.Controllers +{ + internal class ProductUIController + { + static ProductApiClient productApiClient = new(); + static CategoryApiClient categoryApiClient = new(); + internal static async Task AddNewProduct() + { + var categories = await categoryApiClient.GetCategories(); + if (categories is null || categories.Count == 0) + { + Console.WriteLine("No categories found!"); + return; + } + else + TableVisualisation.ShowCategories(categories); + + int id = await CategoryUIService.GetCategoryId("Please choose the id of the category for new product"); + if (id == 0) + { + Console.WriteLine("Category doesn't exist"); + return; + } + + string name = ProductUIService.GetProductName("Please put the name of the new product"); + if (name.ToLower() == "x") return; + + int stock = Helper.GetIntInput("Please put the stock of new product"); + if (stock == 0) return; + if (!Helper.ValidateStock(stock)) + { + Console.WriteLine("Stock must be bigger then 0"); + return; + } + + decimal price = Helper.GetDecimalInput("Please put the price of the product"); + if (price == 0m) return; + if (!Helper.ValidatePrice(price)) + { + Console.WriteLine("Price must be bigger then 0"); + return; + } + + ProductDto product = new() + { + Name = name, + Stock = stock, + Price = price, + CategoryId = id + }; + Console.WriteLine(await productApiClient.CreateProduct(product)); + } + + private static void ViewAllProducts(ref ApiResponseDto>? response, ref int pageNumber, ref int pageSize, ref bool keepRunning) + { + if (response?.Data == null || response.Data.Count == 0) { Console.WriteLine("No products found!"); return; } + + Console.Clear(); + + TableVisualisation.ShowProducts(response.Data); + Console.WriteLine($"Total count: {response.TotalCount}"); + Console.WriteLine($"Current page: {response.CurrentPage}"); + Console.WriteLine($"Page size: {response.PageSize}"); + Console.WriteLine($"Has next: {response.HasNext}"); + Console.WriteLine($"Has previous: {response.HasPrevious}"); + + var choice = AnsiConsole.Prompt(new SelectionPrompt() + .AddChoices("Next", "Previous", "First", "Last", "Go Back")); + + switch (choice) + { + case "Next": + if (!response.HasNext) return; + pageNumber++; + break; + case "Previous": + if (!response.HasPrevious) return; + if (pageNumber > 1) pageNumber--; + break; + case "First": + pageNumber = 1; + break; + case "Last": + pageNumber = response.TotalCount % pageSize == 0 ? response.TotalCount / pageSize : response.TotalCount / pageSize + 1; + break; + case "Go Back": + keepRunning = false; + break; + } + + } + + internal static async Task DeleteProduct() + { + int pageNumber = 1; + int pageSize = 5; + bool keepRunning = true; + + while (keepRunning) + { + var response = await productApiClient.GetProducts(pageNumber, pageSize); + + ViewAllProducts(ref response, ref pageNumber, ref pageSize, ref keepRunning); + } + + int id = Helper.GetIntInput("Please put the id of the product you want to delete"); + var products = await productApiClient.GetProducts(); + if (products is null) + { + Console.WriteLine("No products found"); return; + } + if (products.Data is not null && products.Data.Count == 0) + { + Console.WriteLine("No products found"); + return; + } + + if (Helper.IsProductIdCorrect(id, products.Data)) + Console.WriteLine(await productApiClient.DeleteProduct(id)); + else + Console.WriteLine("Id is incorrect"); + } + + internal static async Task UpdateProduct() + { + int pageNumber = 1; + int pageSize = 5; + bool keepRunning = true; + + while (keepRunning) + { + var response = await productApiClient.GetProducts(pageNumber, pageSize); + + ViewAllProducts(ref response, ref pageNumber, ref pageSize, ref keepRunning); + } + var products = await productApiClient.GetProducts(); + if (products is null) + { + Console.WriteLine("No products found"); + return; + } + if ( products.Data is not null && products.Data.Count == 0) + { + Console.WriteLine("No products found"); + return; + } + + int id = Helper.GetIntInput("Please put the id of the product you want to update"); + if (Helper.IsProductIdCorrect(id, products.Data)) + { + int categoryId = await CategoryUIService.GetCategoryId("Please choose the new id of the category for the product"); + if (categoryId == 0) return; + + string name = ProductUIService.GetProductName("Please put the new name of the product"); + if (name.ToLower() == "x") return; + + int stock = ProductUIService.GetStock("Please put the new stock of the product"); + if (stock == 0) return; + if (!Helper.ValidateStock(stock)) + { + Console.WriteLine("Stock must be bigger then 0"); + return; + } + + ProductUpdateDto product = new() + { + Name = name, + Stock = stock, + CategoryId = categoryId + }; + + Console.WriteLine(await productApiClient.UpdateProduct(id, product)); + } + else + Console.WriteLine("Id is incorrect"); + } + + internal static async Task ViewAllProductsOrderedById() + { + int pageNumber = 1; + int pageSize = 5; + bool keepRunning = true; + + while (keepRunning) + { + var response = await productApiClient.GetProducts(pageNumber, pageSize); + + ViewAllProducts(ref response, ref pageNumber, ref pageSize, ref keepRunning); + } + } + + internal static async Task ViewAllProductsOrderedByPrice() + { + int pageNumber = 1; + int pageSize = 5; + bool keepRunning = true; + + while (keepRunning) + { + var response = await productApiClient.GetProductsOrderedByPrice(pageNumber, pageSize); + + ViewAllProducts(ref response, ref pageNumber, ref pageSize, ref keepRunning); + } + } + + internal static async Task ViewAllProductsOrderedByStock() + { + int pageNumber = 1; + int pageSize = 5; + bool keepRunning = true; + + while (keepRunning) + { + var response = await productApiClient.GetProductsOrderedByStock(pageNumber, pageSize); + + ViewAllProducts(ref response, ref pageNumber, ref pageSize, ref keepRunning); + } + } + + internal static async Task ViewProductById() + { + await ViewAllProductsOrderedById(); + int id = Helper.GetIntInput("Please put the id of the product"); + var products = await productApiClient.GetProducts(); + if(products is not null && Helper.IsProductIdCorrect(id, products.Data)) + { + var product = await productApiClient.GetProductById(id); + if(product is null) + { + Console.WriteLine("Couldn't get product"); + return; + } + TableVisualisation.ShowProduct(product); + } + else + Console.WriteLine("Id is not correct"); + } + + internal static async Task ViewProductsByName() + { + string name = ProductUIService.GetProductName("Please put the name of product you want to view"); + List? products = await productApiClient.GetProductsWithName(name); + + if(products is null || products.Count == 0) + { + Console.WriteLine("No products found"); + return; + } + TableVisualisation.ShowAllProducts(products); + } + + internal static async Task ViewProductsCheaperThenPrice() + { + decimal price = Helper.GetDecimalInput("Please put the price you want to view products"); + + List? products = await productApiClient.GetProductsCheaperThenPrice(price); + + if(products is null || products.Count == 0) + { + Console.WriteLine("No products found"); + return; + } + TableVisualisation.ShowAllProducts(products); + } + + internal static async Task ViewProductsLowerThenStock() + { + int stock = Helper.GetIntInput("PLease put the stock you want to view products"); + + List? products = await productApiClient.ViewProductsLowerThenStock(stock); + + if (products is null || products.Count == 0) + { + Console.WriteLine("No products found"); + return; + } + TableVisualisation.ShowAllProducts(products); + } + } +} diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/Controllers/SaleUIController.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Controllers/SaleUIController.cs new file mode 100644 index 00000000..2defa698 --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Controllers/SaleUIController.cs @@ -0,0 +1,165 @@ +using EcommerceAPI.Ledana.DTOs; +using ECommerceUI.Ledana.Clients; +using ECommerceUI.Ledana.Services; +using ECommerceUI.Ledana.UI; +using Spectre.Console; + +namespace ECommerceUI.Ledana.Controllers +{ + internal class SaleUIController + { + static SaleApiClient saleApiClient = new(); + internal static async Task AddNewSale() + { + DateTime date = DateTime.UtcNow; + + var products = await SaleUIService.GetProducts(); + if (products is null || products.Count == 0) + { + Console.WriteLine("Your products can not be added to a new sale"); + return; + } + + SaleDto saleDto = new() + { + Date = date, + SaleProducts = products + }; + Console.WriteLine(await saleApiClient.CreateSale(saleDto)); + } + + internal static void ViewAllSales(ref ApiResponseDto> response, ref int pageNumber, ref int pageSize, ref bool keepRunning) + { + if (response?.Data == null || response.Data.Count == 0) { Console.WriteLine("No sales found!"); return; } + + Console.Clear(); + + TableVisualisation.ShowSales(response.Data); + Console.WriteLine($"Total count: {response.TotalCount}"); + Console.WriteLine($"Current page: {response.CurrentPage}"); + Console.WriteLine($"Page size: {response.PageSize}"); + Console.WriteLine($"Has next: {response.HasNext}"); + Console.WriteLine($"Has previous: {response.HasPrevious}"); + + var choice = AnsiConsole.Prompt(new SelectionPrompt() + .AddChoices("Next", "Previous", "First", "Last", "Go Back")); + + switch (choice) + { + case "Next": + if (!response.HasNext) return; + pageNumber++; + break; + case "Previous": + if (!response.HasPrevious) return; + if (pageNumber > 1) pageNumber--; + break; + case "First": + pageNumber = 1; + break; + case "Last": + pageNumber = response.TotalCount % pageSize == 0 ? response.TotalCount / pageSize : response.TotalCount / pageSize + 1; + break; + case "Go Back": + keepRunning = false; + break; + } + } + + internal static async Task ViewSalesCheaperThenPrice() + { + decimal price = Helper.GetDecimalInput("Please put the price you want to view"); + var sales = await saleApiClient.GetSalesCheaperThenPrice(price); + if (sales is null || sales.Count == 0) + { + Console.WriteLine("No sales found"); + return; + } + + TableVisualisation.ShowSales(sales); + } + + + internal static async Task ViewSalesOrderedByDate() + { + int pageNumber = 1; + int pageSize = 5; + bool keepRunning = true; + + while (keepRunning) + { + var response = await saleApiClient.GetSalesSortedByDate(pageNumber, pageSize); + if (response is null) + { + Console.WriteLine("Couldn't get sales"); + return; + } + + ViewAllSales(ref response, ref pageNumber, ref pageSize, ref keepRunning); + } + } + + internal static async Task ViewSalesOrderedByTotalPrice() + { + int pageNumber = 1; + int pageSize = 5; + bool keepRunning = true; + + while (keepRunning) + { + var response = await saleApiClient.GetSalesSortedByTotalPrice(pageNumber, pageSize); + if (response is null) + { + Console.WriteLine("Couldn't get sales"); + return; + } + + ViewAllSales(ref response, ref pageNumber, ref pageSize, ref keepRunning); + } + } + + internal static async Task ViewSalesWithCategoryName() + { + string name = ProductUIService.GetProductName("Please put the name of category you want to view sales on"); + var sales = await saleApiClient.GetSalesWithCategoryName(name); + if (sales is null || sales.Count == 0) + { + Console.WriteLine("No sales found"); + return; + } + + TableVisualisation.ShowSales(sales); + } + + + internal static async Task ViewSalesWithProductName() + { + string name = ProductUIService.GetProductName("Please put the name of product you want to view sales on"); + var sales = await saleApiClient.GetSalesWithProductName(name); + if (sales is null || sales.Count == 0) + { + Console.WriteLine("No sales found"); + return; + } + + TableVisualisation.ShowSales(sales); + } + + + internal static async Task ViewSaleWithId() + { + await ViewSalesOrderedByDate(); + int categoryId = Helper.GetIntInput("Please put the id of the sale you want to view"); + if (categoryId == 0) return; + + var sale = await saleApiClient.GetSaleById(categoryId); + if (sale is null) + { + Console.WriteLine("No sale found"); + return; + } + + TableVisualisation.ShowSale(sale); + } + } +} diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/ECommerceUI.Ledana.csproj b/EcommerceAPI.Ledana/ECommerceUI.Ledana/ECommerceUI.Ledana.csproj new file mode 100644 index 00000000..d656cf17 --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/ECommerceUI.Ledana.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/Enums.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Enums.cs new file mode 100644 index 00000000..7ff4f982 --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Enums.cs @@ -0,0 +1,58 @@ +namespace ECommerceUI.Ledana +{ + internal class Enums + { + public enum MainMenuOptions + { + ProductsMenu, + CategoriesMenu, + SalesMenu, + Quit + } + public enum ProductsMenuOptions + { + ViewProductsMenu, + AddNewProduct, + UpdateProduct, + DeleteProduct, + GoBack + } + public enum ViewProductsMenuOptions + { + ViewAllProductsOrderedById, + ViewAllProductsOrderedByPrice, + ViewAllProductsOrderedByStock, + ViewProductById, + ViewProductsByName, + ViewProductsCheaperThenPrice, + ViewProductsLowerThenStock, + GoBack + } + public enum CategoriesMenuOptions + { + ViewCategories, + ViewCategoryById, + AddNewCategory, + UpdateCategory, + DeleteCategory, + GoBack + } + public enum SalesMenuOptions + { + ViewSalesMenu, + AddNewSale, + GoBack + } + public enum ViewSalesMenuOptions + { + ViewSalesOrderedByDate, + ViewSalesOrderedByTotalPrice, + ViewSaleWithId, + ViewSalesWithProductName, + ViewSalesWithCategoryName, + ViewSalesWithTotalPrice, + ViewSalesCheaperThenPrice, + GoBack + } + } +} diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/Helper.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Helper.cs new file mode 100644 index 00000000..123d08fe --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Helper.cs @@ -0,0 +1,50 @@ +using EcommerceAPI.Ledana.Models; +using System.Globalization; + +namespace ECommerceUI.Ledana +{ + internal class Helper + { + internal static int GetIntInput(string message) + { + Console.WriteLine(message); + string? input = Console.ReadLine(); + int id; + while (!int.TryParse(input, out id) || input is null) + { + Console.WriteLine("Invalid input, try again! Or type '0' to go back"); + input = Console.ReadLine(); + } + return id; + } + + internal static bool IsProductIdCorrect(int id, List? products) + { + if (products is null) return false; + + return products.Any(p => p.Id == id); + } + internal static decimal GetDecimalInput(string message) + { + Console.WriteLine(message); + string? input = Console.ReadLine(); + decimal num; + while(!decimal.TryParse(input, out num) || input is null) + { + Console.WriteLine("Invalid input, try again! Or type '0' to go back"); + input = Console.ReadLine(); + } + return num; + } + + internal static bool ValidateStock(int stock) + { + return stock > 0; + } + + internal static bool ValidatePrice(decimal price) + { + return price > 0; + } + } +} diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/Program.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Program.cs new file mode 100644 index 00000000..81cc5560 --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Program.cs @@ -0,0 +1,4 @@ +using ECommerceUI.Ledana.UI; + +UserInterface userInterface = new(); +await userInterface.MainMenu(); diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/Services/CategoryUIService.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Services/CategoryUIService.cs new file mode 100644 index 00000000..aeb461d9 --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Services/CategoryUIService.cs @@ -0,0 +1,37 @@ +using EcommerceAPI.Ledana.Models; +using ECommerceUI.Ledana.Clients; + +namespace ECommerceUI.Ledana.Services +{ + internal class CategoryUIService + { + static CategoryApiClient categoryApiClient = new(); + + internal static async Task GetCategoryId(string message) + { + int id = Helper.GetIntInput(message); + if (id == 0) return 0; + + var categories = await categoryApiClient.GetCategories(); + if (categories is null) return 0; + + if (categories.Any(c => c.Id == id)) + return id; + + Console.WriteLine("Id is not correct"); + return 0; + } + + internal static string GetCategoryName(string message) + { + Console.WriteLine(message); + var input = Console.ReadLine(); + while(string.IsNullOrEmpty(input)) + { + Console.WriteLine(message); + input = Console.ReadLine(); + } + return input; + } + } +} diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/Services/ProductUIService.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Services/ProductUIService.cs new file mode 100644 index 00000000..13391e87 --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Services/ProductUIService.cs @@ -0,0 +1,37 @@ +namespace ECommerceUI.Ledana.Services +{ + internal class ProductUIService + { + + internal static string GetProductName(string message) + { + Console.WriteLine(message); + string? name = Console.ReadLine(); + while(name is null) + { + Console.WriteLine("Please put a valid name or type 'x' to go back"); + name = Console.ReadLine(); + } + return name; + } + + internal static decimal GetProductPrice() + { + Console.WriteLine("Please put the price of new product"); + string? input = Console.ReadLine(); + decimal price; + + while(!decimal.TryParse(input, out price) || input is null) + { + Console.WriteLine("Invalid input, try again or type 'x' to go back"); + input = Console.ReadLine(); + } + return price; + } + + internal static int GetStock(string message) + { + return Helper.GetIntInput(message); + } + } +} diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/Services/SaleUIService.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Services/SaleUIService.cs new file mode 100644 index 00000000..2cd2090f --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/Services/SaleUIService.cs @@ -0,0 +1,71 @@ +using EcommerceAPI.Ledana.DTOs; +using ECommerceUI.Ledana.Clients; +using ECommerceUI.Ledana.UI; +using Spectre.Console; + +namespace ECommerceUI.Ledana.Services +{ + internal class SaleUIService + { + static ProductApiClient productApiClient = new(); + internal static async Task?> GetProducts() + { + bool isRunning = AnsiConsole.Confirm("Do you want to add a product"); + List saleProducts = []; + int quantity; + decimal discount; + decimal totalPrice = 0m; + var products = await productApiClient.GetProductsWithoutPagination(); + if(products is null) + { + Console.WriteLine("Validating products went wrong please close the app and try again"); + return null; + } + + while (isRunning) + { + TableVisualisation.ShowAllProducts(products); + int productId = Helper.GetIntInput("Please put the product id"); + var product = await productApiClient.GetProductById(productId); + if(product is null) + { + Console.WriteLine("Your chosen product is not found"); + return null; + } + + if (Helper.IsProductIdCorrect(productId, products)) + { + Console.WriteLine("\nPrice for product is " + product.Price); + quantity = Helper.GetIntInput("Please put the quantity"); + while (quantity > product.Stock) + { + Console.WriteLine("Quantity is greater than stock of product"); + quantity = Helper.GetIntInput("Please put the quantity"); + } + if (quantity == 0) return []; + + discount = Helper.GetDecimalInput("Please put the discount"); + } + else + { + Console.WriteLine("Input was not valid"); + return null; + } + + SaleProductDto productDto = new() + { + ProductsId = productId, + Quantity = quantity, + Discount = discount + }; + saleProducts.Add(productDto); + totalPrice += product.Price * (1 - discount); + Console.WriteLine("Your total for now is " + totalPrice); + isRunning = AnsiConsole.Confirm("Do you want to add a product?"); + } + Console.WriteLine("\nTotal price is " + totalPrice); + + return saleProducts; + } + } +} diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/UI/TableVisualisation.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/UI/TableVisualisation.cs new file mode 100644 index 00000000..d820bfd7 --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/UI/TableVisualisation.cs @@ -0,0 +1,135 @@ +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Models; +using Spectre.Console; + +namespace ECommerceUI.Ledana.UI +{ + internal class TableVisualisation + { + internal static void ShowAllProducts(List products) + { + var table = new Table(); + table.AddColumn("Id"); + table.AddColumn("Category name"); + table.AddColumn("Product name"); + table.AddColumn("Stock"); + table.AddColumn("Price"); + + foreach (var item in products) + { + table.AddRow(item.Id.ToString(), item.Category.Name, item.Name, item.Stock.ToString(), item.Price.ToString()); + } + AnsiConsole.Write(table); + } + + internal static void ShowCategories(List categories) + { + var table = new Table(); + table.AddColumn("Id"); + table.AddColumn("Name"); + + foreach(var item in categories) + { + table.AddRow(item.Id.ToString(), item.Name); + } + AnsiConsole.Write(table); + } + + internal static void ShowCategory(Category category) + { + var panel = new Panel($@"Category Id: {category.Id} +Category Name: {category.Name} +") + { + Header = new PanelHeader("Category's info"), + Padding = new Padding(2, 2, 2, 2) + }; + var table = new Table(); + table.AddColumn("Product name"); + table.AddColumn("Product stock"); + table.AddColumn("Product price"); + + foreach (var item in category.Products) + { + table.AddRow(item.Name, item.Stock.ToString(), item.Price.ToString()); + } + AnsiConsole.Write(panel); + AnsiConsole.Write(table); + } + + internal static void ShowProduct(Product product) + { + var panel = new Panel($@"Product Id: {product.Id} +Product Name: {product.Name} +Category Id: {product.CategoryId} +Category Name: {product.Category.Name} +Stock: {product.Stock} +Price: {product.Price}") + { + Header = new PanelHeader("Product's info"), + Padding = new Padding(2, 2, 2, 2) + }; + AnsiConsole.Write(panel); + } + + internal static void ShowProducts(List products) + { + var table = new Table(); + table.AddColumn("Id"); + table.AddColumn("Name"); + table.AddColumn("Category Name"); + table.AddColumn("Stock"); + table.AddColumn("Price"); + + foreach (var item in products) + { + table.AddRow(item.Id.ToString(), item.Name, item.Category.Name, item.Stock.ToString(), item.Price.ToString()); + } + AnsiConsole.Write(table); + } + + internal static void ShowSale(SaleProductViewDto sale) + { + var panel = new Panel($@"Sale Id: {sale.SaleId} +Date: {sale.Date} +Total Products: {sale.Products.Sum(p => p.Quantity)} +Total Price: {sale.TotalPrice}" + + ) + { + Header = new PanelHeader("Sale's info"), + Padding = new Padding(2, 2, 2, 2) + }; + + var table = new Table().ShowRowSeparators(); + table.AddColumn("Product Name"); + table.AddColumn("Category Name"); + table.AddColumn("Quantity"); + table.AddColumn("Unit Price at Sale"); + table.AddColumn("Discount"); + table.AddColumn("Product Final Price"); + + foreach (var product in sale.Products) + { + table.AddRow(product.ProductName, product.CategoryName, product.Quantity.ToString(), product.UnitPriceAtSale.ToString(), product.Discount.ToString(), product.TotalPrice.ToString()); + } + AnsiConsole.Write(panel); + AnsiConsole.Write(table); + } + + internal static void ShowSales(List data) + { + var table = new Table().ShowRowSeparators(); + table.AddColumn("Sale Id"); + table.AddColumn("Date"); + table.AddColumn("Total Products"); + table.AddColumn("Total Price"); + + foreach (var item in data) + { + table.AddRow(item.SaleId.ToString(), item.Date.ToString(), item.Products.Sum(p => p.Quantity).ToString(), item.TotalPrice.ToString()); + } + AnsiConsole.Write(table); + } + } +} diff --git a/EcommerceAPI.Ledana/ECommerceUI.Ledana/UI/UserInterface.cs b/EcommerceAPI.Ledana/ECommerceUI.Ledana/UI/UserInterface.cs new file mode 100644 index 00000000..58f00ca9 --- /dev/null +++ b/EcommerceAPI.Ledana/ECommerceUI.Ledana/UI/UserInterface.cs @@ -0,0 +1,249 @@ +using ECommerceUI.Ledana.Controllers; +using Spectre.Console; +using static ECommerceUI.Ledana.Enums; + +namespace ECommerceUI.Ledana.UI +{ + internal class UserInterface + { + internal async Task MainMenu() + { + Console.WriteLine("Welcome to our app!"); + + bool isRunning = true; + while(isRunning) + { + Console.Clear(); + var option = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do?") + .AddChoices( + MainMenuOptions.ProductsMenu, + MainMenuOptions.CategoriesMenu, + MainMenuOptions.SalesMenu, + MainMenuOptions.Quit + )); + + switch(option) + { + case MainMenuOptions.ProductsMenu: + await ProductsMenu(); + break; + case MainMenuOptions.CategoriesMenu: + await CategoriesMenu(); + break; + case MainMenuOptions.SalesMenu: + await SalesMenu(); + break; + case MainMenuOptions.Quit: + Console.WriteLine("Good bye"); + isRunning = false; + break; + } + } + } + + private async Task SalesMenu() + { + bool isRunning = true; + while (isRunning) + { + var option = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do?") + .AddChoices( + SalesMenuOptions.ViewSalesMenu, + SalesMenuOptions.AddNewSale, + SalesMenuOptions.GoBack + )); + + switch (option) + { + case SalesMenuOptions.ViewSalesMenu: + await ViewSalesMenu(); + break; + case SalesMenuOptions.AddNewSale: + await SaleUIController.AddNewSale(); + break;; + case SalesMenuOptions.GoBack: + isRunning = false; + break; + } + } + } + + private async Task ViewSalesMenu() + { + bool isRunning = true; + while (isRunning) + { + var option = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do?") + .AddChoices( + ViewSalesMenuOptions.ViewSalesOrderedByDate, + ViewSalesMenuOptions.ViewSalesOrderedByTotalPrice, + ViewSalesMenuOptions.ViewSaleWithId, + ViewSalesMenuOptions.ViewSalesWithProductName, + ViewSalesMenuOptions.ViewSalesWithCategoryName, + ViewSalesMenuOptions.ViewSalesCheaperThenPrice, + ViewSalesMenuOptions.GoBack + )); + + switch (option) + { + case ViewSalesMenuOptions.ViewSalesOrderedByDate: + await SaleUIController.ViewSalesOrderedByDate(); + break; + case ViewSalesMenuOptions.ViewSalesOrderedByTotalPrice: + await SaleUIController.ViewSalesOrderedByTotalPrice(); + break; + case ViewSalesMenuOptions.ViewSaleWithId: + await SaleUIController.ViewSaleWithId(); + break; + case ViewSalesMenuOptions.ViewSalesWithProductName: + await SaleUIController.ViewSalesWithProductName(); + break; + case ViewSalesMenuOptions.ViewSalesWithCategoryName: + await SaleUIController.ViewSalesWithCategoryName(); + break; + + case ViewSalesMenuOptions.ViewSalesCheaperThenPrice: + await SaleUIController.ViewSalesCheaperThenPrice(); + break; + + case ViewSalesMenuOptions.GoBack: + isRunning = false; + break; + } + } + } + + private async Task CategoriesMenu() + { + bool isRunning = true; + while (isRunning) + { + var option = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do?") + .AddChoices( + CategoriesMenuOptions.ViewCategories, + CategoriesMenuOptions.ViewCategoryById, + CategoriesMenuOptions.AddNewCategory, + CategoriesMenuOptions.UpdateCategory, + CategoriesMenuOptions.DeleteCategory, + CategoriesMenuOptions.GoBack + )); + + switch (option) + { + case CategoriesMenuOptions.ViewCategories: + await CategoryUIController.ViewCategories(); + break; + case CategoriesMenuOptions.ViewCategoryById: + await CategoryUIController.ViewCategoryById(); + break; + case CategoriesMenuOptions.AddNewCategory: + await CategoryUIController.AddNewCategory(); + break; + case CategoriesMenuOptions.UpdateCategory: + await CategoryUIController.UpdateCategory(); + break; + case CategoriesMenuOptions.DeleteCategory: + await CategoryUIController.DeleteCategory(); + break; + case CategoriesMenuOptions.GoBack: + isRunning = false; + break; + } + } + } + + private async Task ProductsMenu() + { + bool isRunning = true; + while (isRunning) + { + var option = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do?") + .AddChoices( + ProductsMenuOptions.ViewProductsMenu, + ProductsMenuOptions.AddNewProduct, + ProductsMenuOptions.UpdateProduct, + ProductsMenuOptions.DeleteProduct, + ProductsMenuOptions.GoBack + )); + + switch (option) + { + case ProductsMenuOptions.ViewProductsMenu: + await ViewProductsMenu(); + break; + case ProductsMenuOptions.AddNewProduct: + await ProductUIController.AddNewProduct(); + break; + case ProductsMenuOptions.UpdateProduct: + await ProductUIController.UpdateProduct(); + break; + case ProductsMenuOptions.DeleteProduct: + await ProductUIController.DeleteProduct(); + break; + case ProductsMenuOptions.GoBack: + isRunning = false; + break; + } + } + } + + private async Task ViewProductsMenu() + { + bool isRunning = true; + while (isRunning) + { + var option = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do?") + .AddChoices( + ViewProductsMenuOptions.ViewAllProductsOrderedById, + ViewProductsMenuOptions.ViewAllProductsOrderedByPrice, + ViewProductsMenuOptions.ViewAllProductsOrderedByStock, + ViewProductsMenuOptions.ViewProductById, + ViewProductsMenuOptions.ViewProductsByName, + ViewProductsMenuOptions.ViewProductsCheaperThenPrice, + ViewProductsMenuOptions.ViewProductsLowerThenStock, + ViewProductsMenuOptions.GoBack + )); + + switch (option) + { + case ViewProductsMenuOptions.ViewAllProductsOrderedById: + await ProductUIController.ViewAllProductsOrderedById(); + break; + case ViewProductsMenuOptions.ViewAllProductsOrderedByPrice: + await ProductUIController.ViewAllProductsOrderedByPrice(); + break; + case ViewProductsMenuOptions.ViewAllProductsOrderedByStock: + await ProductUIController.ViewAllProductsOrderedByStock(); + break; + case ViewProductsMenuOptions.ViewProductById: + await ProductUIController.ViewProductById(); + break; + case ViewProductsMenuOptions.ViewProductsByName: + await ProductUIController.ViewProductsByName(); + break; + case ViewProductsMenuOptions.ViewProductsCheaperThenPrice: + await ProductUIController.ViewProductsCheaperThenPrice(); + break; + case ViewProductsMenuOptions.ViewProductsLowerThenStock: + await ProductUIController.ViewProductsLowerThenStock(); + break; + case ViewProductsMenuOptions.GoBack: + isRunning = false; + break; + } + } + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana.slnx b/EcommerceAPI.Ledana/EcommerceAPI.Ledana.slnx new file mode 100644 index 00000000..6d52f4a9 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Controllers/CategoryController.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Controllers/CategoryController.cs new file mode 100644 index 00000000..cd63455c --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Controllers/CategoryController.cs @@ -0,0 +1,78 @@ +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Interfaces; +using EcommerceAPI.Ledana.Models; +using Microsoft.AspNetCore.Mvc; + +// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 + +namespace EcommerceAPI.Ledana.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class CategoryController : ControllerBase + { + private readonly ICategoryService _categoryService; + public CategoryController(ICategoryService categoryService) + { + _categoryService = categoryService; + } + + [HttpGet] + public async Task>>> Get() + { + ApiResponseDto> response = await _categoryService.GetAllCategories(); + + if (response is null) return BadRequest(); + + return Ok(response); + } + + + [HttpGet("{id}")] + public async Task>> Get(int id) + { + ApiResponseDto response = await _categoryService.GetCategoryById(id); + + if (response is null) return NotFound(); + + return Ok(response); + } + + + [HttpPost] + public async Task>> Post([FromBody] CategoryDto category) + { + if (!ModelState.IsValid) return BadRequest(); + + ApiResponseDto response = await _categoryService.CreateCategory(category); + + if (response is null) return NotFound(); + + return Ok(response); + } + + + [HttpPut("{id}")] + public async Task>> Put(int id, [FromBody] CategoryDto category) + { + if (!ModelState.IsValid) return BadRequest(); + + ApiResponseDto response = await _categoryService.UpdateCategory(id, category); + + if (response is null) return NotFound(); + + return Ok(response); + } + + + [HttpDelete("{id}")] + public async Task>> Delete(int id) + { + var response = await _categoryService.SoftDeleteCategory(id); + + if (response is null) return NotFound(); + + return Ok(response); + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Controllers/ProductController.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Controllers/ProductController.cs new file mode 100644 index 00000000..173d7bd2 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Controllers/ProductController.cs @@ -0,0 +1,88 @@ +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Interfaces; +using EcommerceAPI.Ledana.Models; +using EcommerceAPI.Ledana.Options; +using Microsoft.AspNetCore.Mvc; + +namespace EcommerceAPI.Ledana.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ProductController : ControllerBase + { + private readonly IProductService _productService; + public ProductController(IProductService productService) + { + _productService = productService; + } + + [HttpGet] + public async Task>>> Get([FromQuery] ProductOptions productOptions) + { + var products = await _productService.GetAllProducts(productOptions); + if (products is null) return NotFound(); + + return Ok(products); + } + [HttpGet("all")] + public async Task>>> Get() + { + var products = await _productService.GetAllProducts(); + if (products is null) return NotFound(); + + return Ok(products); + } + + [HttpGet("{id}")] + public async Task>> Get(int id) + { + var product = await _productService.GetProductById(id); + + if (product is null) return NotFound(); + + return Ok(product); + } + + [HttpPost] + public async Task>> Post([FromBody] ProductDto product) + { + if (!ModelState.IsValid) + { + var errors = ModelState + .Where(e => e.Value.Errors.Count > 0) + .ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()); + return BadRequest(errors); + } + + ApiResponseDto result = await _productService.CreateProduct(product); + + return new ObjectResult(result) { StatusCode = 201 }; + } + + + [HttpPut("{id}")] + public async Task?>> Put(int id, [FromBody] ProductUpdateDto product) + { + if (!ModelState.IsValid) return BadRequest(); + + var result = await _productService.UpdateProduct(id, product); + + if (result is null) return NotFound(); + + return Ok(result); + } + + + [HttpDelete("{id}")] + public async Task?>> Delete(int id) + { + var response = await _productService.SoftDeleteProduct(id); + + if (response is null) return NotFound(); + + return Ok(response); + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Controllers/SaleController.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Controllers/SaleController.cs new file mode 100644 index 00000000..f8f33322 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Controllers/SaleController.cs @@ -0,0 +1,58 @@ +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Interfaces; +using EcommerceAPI.Ledana.Models; +using EcommerceAPI.Ledana.Options; +using Microsoft.AspNetCore.Mvc; + +namespace EcommerceAPI.Ledana.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class SaleController : ControllerBase + { + private readonly ISaleService _saleService; + public SaleController(ISaleService saleService) + { + _saleService = saleService; + } + + [HttpGet] + public async Task>>> Get([FromQuery] SaleOptions saleOptions) + { + var response = await _saleService.GetAllSales(saleOptions); + + if (response is null) return BadRequest(); + return Ok(response); + } + [HttpGet("all")] + public async Task>>> Get() + { + var response = await _saleService.GetAllSales(); + + if (response is null) return BadRequest(); + return Ok(response); + } + + [HttpGet("{id}")] + public async Task>> Get(int id) + { + ApiResponseDto response = await _saleService.GetSaleById(id); + + if (response is null) return NotFound(); + return Ok(response); + } + + + [HttpPost] + public async Task>> Post([FromBody] SaleDto sale) + { + if (!ModelState.IsValid) return BadRequest(); + + ApiResponseDto response = await _saleService.CreateSale(sale); + + if (response is null) return BadRequest(response); + return Ok(response); + } + + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/ApiResponseDto.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/ApiResponseDto.cs new file mode 100644 index 00000000..31d58f2e --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/ApiResponseDto.cs @@ -0,0 +1,19 @@ +using System.Net; + +namespace EcommerceAPI.Ledana.DTOs +{ + public class ApiResponseDto + { + public bool RequestFailed { get; set; } = false; + public HttpStatusCode ResponseCode { get; set; } + public string ErrorMessage { get; set; } = string.Empty; + public T? Data { get; set; } + + public int TotalCount { get; set; } + public int CurrentPage { get; set; } + public bool HasNext { get; set; } + + public bool HasPrevious { get; set; } + public int PageSize { get; set; } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/CategoryDto.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/CategoryDto.cs new file mode 100644 index 00000000..30dd8a4c --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/CategoryDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace EcommerceAPI.Ledana.DTOs +{ + public class CategoryDto + { + [Required] + public string Name { get; set; } = null!; + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/ProductDto.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/ProductDto.cs new file mode 100644 index 00000000..12eb5fc3 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/ProductDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace EcommerceAPI.Ledana.DTOs +{ + public class ProductDto + { + [Required] + public string? Name { get; set; } = null!; + [Required] + [Range(1, int.MaxValue, ErrorMessage = "Price must be greater than 0.")] + public decimal? Price { get; set; } + [Required] + [Range(1, int.MaxValue, ErrorMessage = "Stock must be greater than 0.")] + public int? Stock { get; set; } + [Required] + public int? CategoryId { get; set; } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/ProductUpdateDto.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/ProductUpdateDto.cs new file mode 100644 index 00000000..23e32be3 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/ProductUpdateDto.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace EcommerceAPI.Ledana.DTOs +{ + public class ProductUpdateDto + { + [Required] + public string? Name { get; set; } = null!; + [Required] + [Range(1, int.MaxValue, ErrorMessage = "Stock must be greater than 0.")] + public int? Stock { get; set; } + [Required] + public int? CategoryId { get; set; } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/SaleDto.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/SaleDto.cs new file mode 100644 index 00000000..70ef0ce9 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/SaleDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace EcommerceAPI.Ledana.DTOs +{ + public class SaleDto + { + [Required] + public DateTime? Date { get; set; } + [Required] + public List? SaleProducts { get; set; } = []; + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/SaleProductDto.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/SaleProductDto.cs new file mode 100644 index 00000000..30a43699 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/DTOs/SaleProductDto.cs @@ -0,0 +1,38 @@ + +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace EcommerceAPI.Ledana.DTOs +{ + //dto for saleproduct when in use to create a new one + public class SaleProductDto + { + [Required] + public int? ProductsId { get; set; } + [Required] + public int? Quantity { get; set; } + [Required] + public decimal? Discount { get; set; } + + + } + //dto for saleproduct to get in sale + public class SaleProductListDto + { + public string ProductName { get; set; } = null!; + public string CategoryName { get; set; } = null!; + public int Quantity { get; set; } + public decimal Discount { get; set; } + public decimal UnitPriceAtSale { get; set; } + public decimal TotalPrice { get; set; } + } + + //dto for saleproduct when in use to view one + public class SaleProductViewDto + { + public int SaleId { get; set; } + public DateTime Date { get; set; } + public List Products { get; set; } = []; + public decimal TotalPrice { get; set; } //= Products.Sum(p => p.TotalPrice); + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Data/ProductContext.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Data/ProductContext.cs new file mode 100644 index 00000000..f3ae9ed7 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Data/ProductContext.cs @@ -0,0 +1,227 @@ +using EcommerceAPI.Ledana.Models; +using Microsoft.EntityFrameworkCore; + +namespace EcommerceAPI.Ledana.Data +{ + public class ProductContext : DbContext + { + public DbSet Products { get; set; } + public DbSet Categories { get; set; } + public DbSet Sales { get; set; } + + + public ProductContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasQueryFilter(b => !b.IsDeleted); + + modelBuilder.Entity().HasQueryFilter(b => !b.IsDeleted); + + //view in db using entity salewithtotal + modelBuilder.Entity() + .ToView("v_SalesWithTotal") + .HasKey(x => x.Id); + + + modelBuilder.Entity() + .HasMany(p => p.Sales) + .WithMany(s => s.Products) + .UsingEntity( + j => j.HasOne(sp => sp.Sale) + .WithMany(s => s.SaleProducts) + .HasForeignKey(sp => sp.SalesId), + j => j.HasOne(sp => sp.Product) + .WithMany(p => p.SaleProducts) + .HasForeignKey(sp => sp.ProductsId), + j => + { + j.HasKey(sp => new { sp.SalesId, sp.ProductsId }); + j.Property(sp => sp.TotalPrice) + .HasComputedColumnSql( + "([Quantity] * [UnitPriceAtSale]) * (1 - [Discount])", + stored: true + ); + } + ); + + + modelBuilder.Entity().HasData( + new Product() { Id = 1, Name = "Wireless Mouse", Price = 9.99m, Stock = 115, CategoryId = 1 }, + new Product() { Id = 2, Name = "Nose Cancelling HeadPhones", Price = 29.99m, CategoryId = 1 }, + new Product() { Id = 3, Name = "Silent Keyboard", Price = 19.99m, CategoryId = 1 }, + + new Product() { Id = 4, Name = "Curved Monitor", Price = 599.99m, Stock = 89, CategoryId = 2 }, + new Product() { Id = 5, Name = "All Surface Projector", Price = 129.99m, Stock = 115, CategoryId = 2 }, + new Product() { Id = 6, Name = "Wide 4K Monitor", Price = 219.99m, Stock = 120, CategoryId = 2 }, + + new Product() { Id = 7, Name = "Gaming Laptop", Price = 899.99m, Stock = 115, CategoryId = 3 }, + new Product() { Id = 8, Name = "Workstation PC", Price = 1029.99m, Stock = 250, CategoryId = 3 }, + + new Product() { Id = 9, Name = "CPU", Price = 299.99m, Stock = 115, CategoryId = 4 }, + new Product() { Id = 10, Name = "GPU", Price = 109.99m, Stock = 200, CategoryId = 4 }, + new Product() { Id = 11, Name = "RAM", Price = 489.99m, Stock = 115, CategoryId = 4 }, + new Product() { Id = 12, Name = "SSD", Price = 119.99m, Stock = 170, CategoryId = 4 }, + + new Product() { Id = 13, Name = "Gaming Headset", Price = 89.99m, Stock = 115, CategoryId = 5 }, + new Product() { Id = 14, Name = "Studio Mic", Price = 199.99m, Stock = 200, CategoryId = 5 }, + + new Product() { Id = 15, Name = "Webcam", Price = 215.99m, Stock = 115, CategoryId = 6 }, + new Product() { Id = 16, Name = "Capture Card", Price = 708.99m, Stock = 220, CategoryId = 6 }, + new Product() { Id = 17, Name = "360 Camera", Price = 4009.99m, Stock = 115, CategoryId = 6 } + ); + + modelBuilder.Entity().HasData( + new Category() { Id = 1, Name = "Peripherals" }, + new Category() { Id = 2, Name = "Monitors and Displays" }, + new Category() { Id = 3, Name = "Computers and Laptops" }, + new Category() { Id = 4, Name = "Components" }, + new Category() { Id = 5, Name = "Audio and Headsets" }, + new Category() { Id = 6, Name = "Video and Cameras" } + ); + + modelBuilder.Entity().HasData(new Sale() { Id = 1, Date = new DateTime(2026, 04, 30, 12, 00, 00) }, + new Sale() { Id = 2, Date = new DateTime(2026, 05, 01, 12, 00, 00) }, new Sale() { Id = 3, Date = new DateTime(2026, 05, 02, 12, 00, 00) }, + new Sale() { Id = 4, Date = new DateTime(2026, 05, 03, 12, 00, 00) }, new Sale() { Id = 5, Date = new DateTime(2026, 05, 04, 12, 00, 00) } + ); + + modelBuilder.Entity().HasData( + new SaleProduct() + { + ProductsId = 1, + SalesId = 1, + Quantity = 2, + UnitPriceAtSale = 9.99m, + Discount = 0.10m + }, + new SaleProduct() + { + ProductsId = 1, + SalesId = 2, + Quantity = 1, + UnitPriceAtSale = 9.99m, + Discount = 0m + }, + new SaleProduct() + { + ProductsId = 3, + SalesId = 1, + Quantity = 2, + UnitPriceAtSale = 19.99m, + Discount = 0.05m + }, + new SaleProduct() + { + ProductsId = 4, + SalesId = 2, + Quantity = 2, + UnitPriceAtSale = 599.99m, + Discount = 0.20m + }, + + new SaleProduct() + { + ProductsId = 5, + SalesId = 3, + Quantity = 1, + UnitPriceAtSale = 129.99m, + Discount = 0.10m + }, + new SaleProduct() + { + ProductsId = 7, + SalesId = 3, + Quantity = 1, + UnitPriceAtSale = 899.99m, + Discount = 0m + }, + new SaleProduct() + { + ProductsId = 8, + SalesId = 3, + Quantity = 1, + UnitPriceAtSale = 1029.99m, + Discount = 0.05m + }, + new SaleProduct() + { + ProductsId = 5, + SalesId = 2, + Quantity = 2, + UnitPriceAtSale = 129.99m, + Discount = 0.20m + }, + + new SaleProduct() + { + ProductsId = 15, + SalesId = 4, + Quantity = 1, + UnitPriceAtSale = 215.99m, + Discount = 0.10m + }, + new SaleProduct() + { + ProductsId = 9, + SalesId = 4, + Quantity = 1, + UnitPriceAtSale = 299.99m, + Discount = 0m + }, + new SaleProduct() + { + ProductsId = 8, + SalesId = 4, + Quantity = 1, + UnitPriceAtSale = 1029.99m, + Discount = 0.05m + }, + new SaleProduct() + { + ProductsId = 5, + SalesId = 4, + Quantity = 2, + UnitPriceAtSale = 129.99m, + Discount = 0.20m + }, + + new SaleProduct() + { + ProductsId = 11, + SalesId = 5, + Quantity = 1, + UnitPriceAtSale = 489.99m, + Discount = 0.10m + }, + new SaleProduct() + { + ProductsId = 12, + SalesId = 5, + Quantity = 1, + UnitPriceAtSale = 119.99m, + Discount = 0m + }, + new SaleProduct() + { + ProductsId = 17, + SalesId = 4, + Quantity = 1, + UnitPriceAtSale = 4009.99m, + Discount = 0.05m + }, + new SaleProduct() + { + ProductsId = 1, + SalesId = 5, + Quantity = 2, + UnitPriceAtSale = 9.99m, + Discount = 0.20m + } + ); + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/ECommerce.postman_collection.json b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/ECommerce.postman_collection.json new file mode 100644 index 00000000..9eef5bab --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/ECommerce.postman_collection.json @@ -0,0 +1,526 @@ +{ + "info": { + "_postman_id": "15e3ecd5-e397-4636-9b58-4fc3c10693e2", + "name": "ECommerce", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "54339121", + "_collection_link": "https://go.postman.co/collection/54339121-15e3ecd5-e397-4636-9b58-4fc3c10693e2?source=collection_link" + }, + "item": [ + { + "name": "Products", + "item": [ + { + "name": "GetProducts", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7077/api/product", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "product" + ] + } + }, + "response": [] + }, + { + "name": "GetProductById", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7077/api/product/15", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "product", + "15" + ] + } + }, + "response": [] + }, + { + "name": "PostProduct", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n\"name\": \"ColorFull Keyboard\",\r\n\"price\": 45.99,\r\n\"stock\": 100,\r\n\"categoryId\": 1\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7077/api/product", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "product" + ] + } + }, + "response": [] + }, + { + "name": "UpdateProduct", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n\"name\": \"Colorfull Keyboard\",\r\n\"stock\": 99,\r\n\"categoryId\": 1\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7077/api/product/20", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "product", + "20" + ] + } + }, + "response": [] + }, + { + "name": "DeleteProduct", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "https://localhost:7077/api/product/19", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "product", + "19" + ] + } + }, + "response": [] + }, + { + "name": "GetProductsByCategoryNameAndSortByNameInAscendingOrder", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7077/api/product?category=Video and Cameras&sort_by=name&", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "product" + ], + "query": [ + { + "key": "category", + "value": "Video and Cameras" + }, + { + "key": "sort_by", + "value": "name" + }, + { + "key": "", + "value": null + } + ] + } + }, + "response": [] + }, + { + "name": "GetProductsWithPagination", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "https://localhost:7077/api/product?page_size=3&page_number=2", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "product" + ], + "query": [ + { + "key": "page_size", + "value": "3" + }, + { + "key": "page_number", + "value": "2" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Categories", + "item": [ + { + "name": "GetCategories", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7077/api/category", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "category" + ] + } + }, + "response": [] + }, + { + "name": "GetCategoryById", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7077/api/category/6", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "category", + "6" + ] + } + }, + "response": [] + }, + { + "name": "PostCategory", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"food\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7077/api/category", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "category" + ] + } + }, + "response": [] + }, + { + "name": "UpdateCategory", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Super Food\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7077/api/category/7", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "category", + "7" + ] + } + }, + "response": [] + }, + { + "name": "DeleteCategory", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Super Food\"\r\n}" + }, + "url": { + "raw": "https://localhost:7077/api/category/8", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "category", + "8" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Sales", + "item": [ + { + "name": "GetSales", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7077/api/sale", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "sale" + ] + } + }, + "response": [] + }, + { + "name": "GetSaleById", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7077/api/sale/10", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "sale", + "10" + ] + } + }, + "response": [] + }, + { + "name": "PostSale", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"date\": \"2026-05-15T12:00:00\",\r\n \"saleProducts\":\r\n [\r\n {\r\n \"ProductsId\": 4,\r\n \"Quantity\": 2,\r\n \"Discount\": 0.10\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7077/api/sale", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "sale" + ] + } + }, + "response": [] + }, + { + "name": "GetSalesWithPagination", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7077/api/sale?page_size=3&page_number=2", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "sale" + ], + "query": [ + { + "key": "page_size", + "value": "3" + }, + { + "key": "page_number", + "value": "2" + } + ] + } + }, + "response": [] + }, + { + "name": "GetSalesFilteredWithProductName", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7077/api/sale?product_name=CPU", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "sale" + ], + "query": [ + { + "key": "product_name", + "value": "CPU" + } + ] + } + }, + "response": [] + }, + { + "name": "GetSalesSortedByTotalPrice", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"date\": \"2026-05-15T12:00:00\",\r\n \"saleProducts\":\r\n [\r\n {\r\n \"ProductsId\": 4,\r\n \"Quantity\": 2,\r\n \"Discount\": 0.10,\r\n \"UnitPriceAtSale\": 599.99\r\n }\r\n ]\r\n}" + }, + "url": { + "raw": "https://localhost:7077/api/sale?sort_by=total_price", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7077", + "path": [ + "api", + "sale" + ], + "query": [ + { + "key": "sort_by", + "value": "total_price" + } + ] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/EcommerceAPI.Ledana.csproj b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/EcommerceAPI.Ledana.csproj new file mode 100644 index 00000000..8b96b700 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/EcommerceAPI.Ledana.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/EcommerceAPI.Ledana.http b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/EcommerceAPI.Ledana.http new file mode 100644 index 00000000..ae579265 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/EcommerceAPI.Ledana.http @@ -0,0 +1,46 @@ +@EcommerceAPI.Ledana_HostAddress = https://localhost:7077 + +### + +GET {{EcommerceAPI.Ledana_HostAddress}}/api/product/18 + +### + +POST {{EcommerceAPI.Ledana_HostAddress}}/api/product +Content-Type: application/json + +{ +"name": "ColorFull Keyboard", +"price": 45.99, +"categoryId": 1 +} + +### + + +PUT {{EcommerceAPI.Ledana_HostAddress}}/api/product/27 +Content-Type: application/json + +{ +"name": "ColorFull Keyboard", +"price": 15.99, +"stock": 150, +"categoryId": 1 +} +### + +GET {{EcommerceAPI.Ledana_HostAddress}}/api/sale + +### + +@id=25 +PUT {{EcommerceAPI.Ledana_HostAddress}}/api/product/{{id}} +Content-Type: application/json + +{ + "name": "ColorFull Keyboard", +"stock": 150, +"categoryId": 1 +} + +### diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Interfaces/ICategoryService.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Interfaces/ICategoryService.cs new file mode 100644 index 00000000..ed0397db --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Interfaces/ICategoryService.cs @@ -0,0 +1,16 @@ +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Models; + +namespace EcommerceAPI.Ledana.Interfaces +{ + public interface ICategoryService + { + + Task>> GetAllCategories(); + Task> GetCategoryById(int id); + Task> CreateCategory(CategoryDto category); + Task> UpdateCategory(int id, CategoryDto category); + Task> SoftDeleteCategory(int id); + + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Interfaces/IProductService.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Interfaces/IProductService.cs new file mode 100644 index 00000000..cdcc2f88 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Interfaces/IProductService.cs @@ -0,0 +1,16 @@ +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Models; +using EcommerceAPI.Ledana.Options; + +namespace EcommerceAPI.Ledana.Interfaces +{ + public interface IProductService + { + Task> CreateProduct(ProductDto product); + Task> SoftDeleteProduct(int id); + Task?>> GetAllProducts(ProductOptions productOptions); + Task?>> GetAllProducts(); + Task> GetProductById(int id); + Task?> UpdateProduct(int id, ProductUpdateDto product); + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Interfaces/ISaleService.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Interfaces/ISaleService.cs new file mode 100644 index 00000000..65ebe009 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Interfaces/ISaleService.cs @@ -0,0 +1,14 @@ +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Models; +using EcommerceAPI.Ledana.Options; + +namespace EcommerceAPI.Ledana.Interfaces +{ + public interface ISaleService + { + Task> CreateSale(SaleDto sale); + Task>> GetAllSales(SaleOptions saleOptions); + Task>> GetAllSales(); + Task> GetSaleById(int id); + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Interfaces/ISoftDeletable.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Interfaces/ISoftDeletable.cs new file mode 100644 index 00000000..6df449a5 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Interfaces/ISoftDeletable.cs @@ -0,0 +1,8 @@ +namespace EcommerceAPI.Ledana.Interfaces +{ + public interface ISoftDeletable + { + public bool IsDeleted { get; set; } + DateTime? DeletedOnUtc { get; set; } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Mappings/MappingProfile.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Mappings/MappingProfile.cs new file mode 100644 index 00000000..ebfe4a7a --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Mappings/MappingProfile.cs @@ -0,0 +1,18 @@ +using AutoMapper; +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Models; + +namespace EcommerceAPI.Ledana.Mappings +{ + public class MappingProfile : Profile + { + public MappingProfile() + { + CreateMap< ProductDto, Product>(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260515121617_InitialCreate.Designer.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260515121617_InitialCreate.Designer.cs new file mode 100644 index 00000000..76c73de3 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260515121617_InitialCreate.Designer.cs @@ -0,0 +1,488 @@ +// +using System; +using EcommerceAPI.Ledana.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EcommerceAPI.Ledana.Migrations +{ + [DbContext(typeof(ProductContext))] + [Migration("20260515121617_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + Name = "Peripherals" + }, + new + { + Id = 2, + Name = "Monitors and Displays" + }, + new + { + Id = 3, + Name = "Computers and Laptops" + }, + new + { + Id = 4, + Name = "Components" + }, + new + { + Id = 5, + Name = "Audio and Headsets" + }, + new + { + Id = 6, + Name = "Video and Cameras" + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.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") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + Name = "Wireless Mouse", + Price = 9.99m + }, + new + { + Id = 2, + CategoryId = 1, + Name = "Nose Cancelling HeadPhones", + Price = 29.99m + }, + new + { + Id = 3, + CategoryId = 1, + Name = "Silent Keyboard", + Price = 19.99m + }, + new + { + Id = 4, + CategoryId = 2, + Name = "Curved Monitor", + Price = 599.99m + }, + new + { + Id = 5, + CategoryId = 2, + Name = "All Surface Projector", + Price = 129.99m + }, + new + { + Id = 6, + CategoryId = 2, + Name = "Wide 4K Monitor", + Price = 219.99m + }, + new + { + Id = 7, + CategoryId = 3, + Name = "Gaming Laptop", + Price = 899.99m + }, + new + { + Id = 8, + CategoryId = 3, + Name = "Workstation PC", + Price = 1029.99m + }, + new + { + Id = 9, + CategoryId = 4, + Name = "CPU", + Price = 299.99m + }, + new + { + Id = 10, + CategoryId = 4, + Name = "GPU", + Price = 109.99m + }, + new + { + Id = 11, + CategoryId = 4, + Name = "RAM", + Price = 489.99m + }, + new + { + Id = 12, + CategoryId = 4, + Name = "SSD", + Price = 119.99m + }, + new + { + Id = 13, + CategoryId = 5, + Name = "Gaming Headset", + Price = 89.99m + }, + new + { + Id = 14, + CategoryId = 5, + Name = "Studio Mic", + Price = 199.99m + }, + new + { + Id = 15, + CategoryId = 6, + Name = "Webcam", + Price = 215.99m + }, + new + { + Id = 16, + CategoryId = 6, + Name = "Capture Card", + Price = 708.99m + }, + new + { + Id = 17, + CategoryId = 6, + Name = "360 Camera", + Price = 4009.99m + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Sales"); + + b.HasData( + new + { + Id = 1, + Date = new DateTime(2026, 4, 30, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + Date = new DateTime(2026, 5, 1, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 3, + Date = new DateTime(2026, 5, 2, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 4, + Date = new DateTime(2026, 5, 3, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 5, + Date = new DateTime(2026, 5, 4, 12, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.SaleProduct", b => + { + b.Property("SalesId") + .HasColumnType("int"); + + b.Property("ProductsId") + .HasColumnType("int"); + + b.Property("Discount") + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("TotalPrice") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("decimal(18,2)") + .HasComputedColumnSql("([Quantity] * [UnitPriceAtSale]) * (1 - [Discount])", true); + + b.Property("UnitPriceAtSale") + .HasColumnType("decimal(18,2)"); + + b.HasKey("SalesId", "ProductsId"); + + b.HasIndex("ProductsId"); + + b.ToTable("SaleProduct"); + + b.HasData( + new + { + SalesId = 1, + ProductsId = 1, + Discount = 0.10m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }, + new + { + SalesId = 2, + ProductsId = 1, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }, + new + { + SalesId = 1, + ProductsId = 3, + Discount = 0.05m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 19.99m + }, + new + { + SalesId = 2, + ProductsId = 4, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 599.99m + }, + new + { + SalesId = 3, + ProductsId = 5, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 3, + ProductsId = 7, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 899.99m + }, + new + { + SalesId = 3, + ProductsId = 8, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 1029.99m + }, + new + { + SalesId = 2, + ProductsId = 5, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 4, + ProductsId = 15, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 215.99m + }, + new + { + SalesId = 4, + ProductsId = 9, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 299.99m + }, + new + { + SalesId = 4, + ProductsId = 8, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 1029.99m + }, + new + { + SalesId = 4, + ProductsId = 5, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 5, + ProductsId = 11, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 489.99m + }, + new + { + SalesId = 5, + ProductsId = 12, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 119.99m + }, + new + { + SalesId = 4, + ProductsId = 17, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 4009.99m + }, + new + { + SalesId = 5, + ProductsId = 1, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.HasOne("EcommerceAPI.Ledana.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.SaleProduct", b => + { + b.HasOne("EcommerceAPI.Ledana.Models.Product", "Product") + .WithMany("SaleProducts") + .HasForeignKey("ProductsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EcommerceAPI.Ledana.Models.Sale", "Sale") + .WithMany("SaleProducts") + .HasForeignKey("SalesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Sale"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Category", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.Navigation("SaleProducts"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Sale", b => + { + b.Navigation("SaleProducts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260515121617_InitialCreate.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260515121617_InitialCreate.cs new file mode 100644 index 00000000..f11e22c4 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260515121617_InitialCreate.cs @@ -0,0 +1,190 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace EcommerceAPI.Ledana.Migrations +{ + /// + public partial class InitialCreate : 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(max)", 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"), + Date = 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)", 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.Cascade); + }); + + migrationBuilder.CreateTable( + name: "SaleProduct", + columns: table => new + { + ProductsId = table.Column(type: "int", nullable: false), + SalesId = table.Column(type: "int", nullable: false), + Quantity = table.Column(type: "int", nullable: false), + Discount = table.Column(type: "decimal(18,2)", nullable: false), + UnitPriceAtSale = table.Column(type: "decimal(18,2)", nullable: false), + TotalPrice = table.Column(type: "decimal(18,2)", nullable: false, computedColumnSql: "([Quantity] * [UnitPriceAtSale]) * (1 - [Discount])", stored: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SaleProduct", x => new { x.SalesId, x.ProductsId }); + table.ForeignKey( + name: "FK_SaleProduct_Products_ProductsId", + column: x => x.ProductsId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SaleProduct_Sales_SalesId", + column: x => x.SalesId, + principalTable: "Sales", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "Categories", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { 1, "Peripherals" }, + { 2, "Monitors and Displays" }, + { 3, "Computers and Laptops" }, + { 4, "Components" }, + { 5, "Audio and Headsets" }, + { 6, "Video and Cameras" } + }); + + migrationBuilder.InsertData( + table: "Sales", + columns: new[] { "Id", "Date" }, + values: new object[,] + { + { 1, new DateTime(2026, 4, 30, 12, 0, 0, 0, DateTimeKind.Unspecified) }, + { 2, new DateTime(2026, 5, 1, 12, 0, 0, 0, DateTimeKind.Unspecified) }, + { 3, new DateTime(2026, 5, 2, 12, 0, 0, 0, DateTimeKind.Unspecified) }, + { 4, new DateTime(2026, 5, 3, 12, 0, 0, 0, DateTimeKind.Unspecified) }, + { 5, new DateTime(2026, 5, 4, 12, 0, 0, 0, DateTimeKind.Unspecified) } + }); + + migrationBuilder.InsertData( + table: "Products", + columns: new[] { "Id", "CategoryId", "Name", "Price" }, + values: new object[,] + { + { 1, 1, "Wireless Mouse", 9.99m }, + { 2, 1, "Nose Cancelling HeadPhones", 29.99m }, + { 3, 1, "Silent Keyboard", 19.99m }, + { 4, 2, "Curved Monitor", 599.99m }, + { 5, 2, "All Surface Projector", 129.99m }, + { 6, 2, "Wide 4K Monitor", 219.99m }, + { 7, 3, "Gaming Laptop", 899.99m }, + { 8, 3, "Workstation PC", 1029.99m }, + { 9, 4, "CPU", 299.99m }, + { 10, 4, "GPU", 109.99m }, + { 11, 4, "RAM", 489.99m }, + { 12, 4, "SSD", 119.99m }, + { 13, 5, "Gaming Headset", 89.99m }, + { 14, 5, "Studio Mic", 199.99m }, + { 15, 6, "Webcam", 215.99m }, + { 16, 6, "Capture Card", 708.99m }, + { 17, 6, "360 Camera", 4009.99m } + }); + + migrationBuilder.InsertData( + table: "SaleProduct", + columns: new[] { "ProductsId", "SalesId", "Discount", "Quantity", "UnitPriceAtSale" }, + values: new object[,] + { + { 1, 1, 0.10m, 2, 9.99m }, + { 3, 1, 0.05m, 2, 19.99m }, + { 1, 2, 0m, 1, 9.99m }, + { 4, 2, 0.20m, 2, 599.99m }, + { 5, 2, 0.20m, 2, 129.99m }, + { 5, 3, 0.10m, 1, 129.99m }, + { 7, 3, 0m, 1, 899.99m }, + { 8, 3, 0.05m, 1, 1029.99m }, + { 5, 4, 0.20m, 2, 129.99m }, + { 8, 4, 0.05m, 1, 1029.99m }, + { 9, 4, 0m, 1, 299.99m }, + { 15, 4, 0.10m, 1, 215.99m }, + { 17, 4, 0.05m, 1, 4009.99m }, + { 1, 5, 0.20m, 2, 9.99m }, + { 11, 5, 0.10m, 1, 489.99m }, + { 12, 5, 0m, 1, 119.99m } + }); + + migrationBuilder.CreateIndex( + name: "IX_Products_CategoryId", + table: "Products", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_SaleProduct_ProductsId", + table: "SaleProduct", + column: "ProductsId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SaleProduct"); + + migrationBuilder.DropTable( + name: "Products"); + + migrationBuilder.DropTable( + name: "Sales"); + + migrationBuilder.DropTable( + name: "Categories"); + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260516120417_AddSoftDeleteForProductsAndCategories.Designer.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260516120417_AddSoftDeleteForProductsAndCategories.Designer.cs new file mode 100644 index 00000000..752e8b88 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260516120417_AddSoftDeleteForProductsAndCategories.Designer.cs @@ -0,0 +1,523 @@ +// +using System; +using EcommerceAPI.Ledana.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EcommerceAPI.Ledana.Migrations +{ + [DbContext(typeof(ProductContext))] + [Migration("20260516120417_AddSoftDeleteForProductsAndCategories")] + partial class AddSoftDeleteForProductsAndCategories + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DeletedOnUtc") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + IsDeleted = false, + Name = "Peripherals" + }, + new + { + Id = 2, + IsDeleted = false, + Name = "Monitors and Displays" + }, + new + { + Id = 3, + IsDeleted = false, + Name = "Computers and Laptops" + }, + new + { + Id = 4, + IsDeleted = false, + Name = "Components" + }, + new + { + Id = 5, + IsDeleted = false, + Name = "Audio and Headsets" + }, + new + { + Id = 6, + IsDeleted = false, + Name = "Video and Cameras" + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("DeletedOnUtc") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + IsDeleted = false, + Name = "Wireless Mouse", + Price = 9.99m + }, + new + { + Id = 2, + CategoryId = 1, + IsDeleted = false, + Name = "Nose Cancelling HeadPhones", + Price = 29.99m + }, + new + { + Id = 3, + CategoryId = 1, + IsDeleted = false, + Name = "Silent Keyboard", + Price = 19.99m + }, + new + { + Id = 4, + CategoryId = 2, + IsDeleted = false, + Name = "Curved Monitor", + Price = 599.99m + }, + new + { + Id = 5, + CategoryId = 2, + IsDeleted = false, + Name = "All Surface Projector", + Price = 129.99m + }, + new + { + Id = 6, + CategoryId = 2, + IsDeleted = false, + Name = "Wide 4K Monitor", + Price = 219.99m + }, + new + { + Id = 7, + CategoryId = 3, + IsDeleted = false, + Name = "Gaming Laptop", + Price = 899.99m + }, + new + { + Id = 8, + CategoryId = 3, + IsDeleted = false, + Name = "Workstation PC", + Price = 1029.99m + }, + new + { + Id = 9, + CategoryId = 4, + IsDeleted = false, + Name = "CPU", + Price = 299.99m + }, + new + { + Id = 10, + CategoryId = 4, + IsDeleted = false, + Name = "GPU", + Price = 109.99m + }, + new + { + Id = 11, + CategoryId = 4, + IsDeleted = false, + Name = "RAM", + Price = 489.99m + }, + new + { + Id = 12, + CategoryId = 4, + IsDeleted = false, + Name = "SSD", + Price = 119.99m + }, + new + { + Id = 13, + CategoryId = 5, + IsDeleted = false, + Name = "Gaming Headset", + Price = 89.99m + }, + new + { + Id = 14, + CategoryId = 5, + IsDeleted = false, + Name = "Studio Mic", + Price = 199.99m + }, + new + { + Id = 15, + CategoryId = 6, + IsDeleted = false, + Name = "Webcam", + Price = 215.99m + }, + new + { + Id = 16, + CategoryId = 6, + IsDeleted = false, + Name = "Capture Card", + Price = 708.99m + }, + new + { + Id = 17, + CategoryId = 6, + IsDeleted = false, + Name = "360 Camera", + Price = 4009.99m + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Sales"); + + b.HasData( + new + { + Id = 1, + Date = new DateTime(2026, 4, 30, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + Date = new DateTime(2026, 5, 1, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 3, + Date = new DateTime(2026, 5, 2, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 4, + Date = new DateTime(2026, 5, 3, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 5, + Date = new DateTime(2026, 5, 4, 12, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.SaleProduct", b => + { + b.Property("SalesId") + .HasColumnType("int"); + + b.Property("ProductsId") + .HasColumnType("int"); + + b.Property("Discount") + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("TotalPrice") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("decimal(18,2)") + .HasComputedColumnSql("([Quantity] * [UnitPriceAtSale]) * (1 - [Discount])", true); + + b.Property("UnitPriceAtSale") + .HasColumnType("decimal(18,2)"); + + b.HasKey("SalesId", "ProductsId"); + + b.HasIndex("ProductsId"); + + b.ToTable("SaleProduct"); + + b.HasData( + new + { + SalesId = 1, + ProductsId = 1, + Discount = 0.10m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }, + new + { + SalesId = 2, + ProductsId = 1, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }, + new + { + SalesId = 1, + ProductsId = 3, + Discount = 0.05m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 19.99m + }, + new + { + SalesId = 2, + ProductsId = 4, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 599.99m + }, + new + { + SalesId = 3, + ProductsId = 5, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 3, + ProductsId = 7, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 899.99m + }, + new + { + SalesId = 3, + ProductsId = 8, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 1029.99m + }, + new + { + SalesId = 2, + ProductsId = 5, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 4, + ProductsId = 15, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 215.99m + }, + new + { + SalesId = 4, + ProductsId = 9, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 299.99m + }, + new + { + SalesId = 4, + ProductsId = 8, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 1029.99m + }, + new + { + SalesId = 4, + ProductsId = 5, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 5, + ProductsId = 11, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 489.99m + }, + new + { + SalesId = 5, + ProductsId = 12, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 119.99m + }, + new + { + SalesId = 4, + ProductsId = 17, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 4009.99m + }, + new + { + SalesId = 5, + ProductsId = 1, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.HasOne("EcommerceAPI.Ledana.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.SaleProduct", b => + { + b.HasOne("EcommerceAPI.Ledana.Models.Product", "Product") + .WithMany("SaleProducts") + .HasForeignKey("ProductsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EcommerceAPI.Ledana.Models.Sale", "Sale") + .WithMany("SaleProducts") + .HasForeignKey("SalesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Sale"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Category", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.Navigation("SaleProducts"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Sale", b => + { + b.Navigation("SaleProducts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260516120417_AddSoftDeleteForProductsAndCategories.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260516120417_AddSoftDeleteForProductsAndCategories.cs new file mode 100644 index 00000000..45a3d5dd --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260516120417_AddSoftDeleteForProductsAndCategories.cs @@ -0,0 +1,222 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EcommerceAPI.Ledana.Migrations +{ + /// + public partial class AddSoftDeleteForProductsAndCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DeletedOnUtc", + table: "Products", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Products", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DeletedOnUtc", + table: "Categories", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Categories", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 2, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 3, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 4, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 5, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Categories", + keyColumn: "Id", + keyValue: 6, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 2, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 3, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 4, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 5, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 6, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 7, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 8, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 9, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 10, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 11, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 12, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 13, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 14, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 15, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 16, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 17, + columns: new[] { "DeletedOnUtc", "IsDeleted" }, + values: new object[] { null, false }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DeletedOnUtc", + table: "Products"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Products"); + + migrationBuilder.DropColumn( + name: "DeletedOnUtc", + table: "Categories"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Categories"); + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260516130201_AddedStockInProducts.Designer.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260516130201_AddedStockInProducts.Designer.cs new file mode 100644 index 00000000..cc6467f3 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260516130201_AddedStockInProducts.Designer.cs @@ -0,0 +1,543 @@ +// +using System; +using EcommerceAPI.Ledana.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EcommerceAPI.Ledana.Migrations +{ + [DbContext(typeof(ProductContext))] + [Migration("20260516130201_AddedStockInProducts")] + partial class AddedStockInProducts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DeletedOnUtc") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + IsDeleted = false, + Name = "Peripherals" + }, + new + { + Id = 2, + IsDeleted = false, + Name = "Monitors and Displays" + }, + new + { + Id = 3, + IsDeleted = false, + Name = "Computers and Laptops" + }, + new + { + Id = 4, + IsDeleted = false, + Name = "Components" + }, + new + { + Id = 5, + IsDeleted = false, + Name = "Audio and Headsets" + }, + new + { + Id = 6, + IsDeleted = false, + Name = "Video and Cameras" + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("DeletedOnUtc") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Stock") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + IsDeleted = false, + Name = "Wireless Mouse", + Price = 9.99m, + Stock = 115 + }, + new + { + Id = 2, + CategoryId = 1, + IsDeleted = false, + Name = "Nose Cancelling HeadPhones", + Price = 29.99m, + Stock = 0 + }, + new + { + Id = 3, + CategoryId = 1, + IsDeleted = false, + Name = "Silent Keyboard", + Price = 19.99m, + Stock = 0 + }, + new + { + Id = 4, + CategoryId = 2, + IsDeleted = false, + Name = "Curved Monitor", + Price = 599.99m, + Stock = 89 + }, + new + { + Id = 5, + CategoryId = 2, + IsDeleted = false, + Name = "All Surface Projector", + Price = 129.99m, + Stock = 115 + }, + new + { + Id = 6, + CategoryId = 2, + IsDeleted = false, + Name = "Wide 4K Monitor", + Price = 219.99m, + Stock = 120 + }, + new + { + Id = 7, + CategoryId = 3, + IsDeleted = false, + Name = "Gaming Laptop", + Price = 899.99m, + Stock = 115 + }, + new + { + Id = 8, + CategoryId = 3, + IsDeleted = false, + Name = "Workstation PC", + Price = 1029.99m, + Stock = 250 + }, + new + { + Id = 9, + CategoryId = 4, + IsDeleted = false, + Name = "CPU", + Price = 299.99m, + Stock = 115 + }, + new + { + Id = 10, + CategoryId = 4, + IsDeleted = false, + Name = "GPU", + Price = 109.99m, + Stock = 200 + }, + new + { + Id = 11, + CategoryId = 4, + IsDeleted = false, + Name = "RAM", + Price = 489.99m, + Stock = 115 + }, + new + { + Id = 12, + CategoryId = 4, + IsDeleted = false, + Name = "SSD", + Price = 119.99m, + Stock = 170 + }, + new + { + Id = 13, + CategoryId = 5, + IsDeleted = false, + Name = "Gaming Headset", + Price = 89.99m, + Stock = 115 + }, + new + { + Id = 14, + CategoryId = 5, + IsDeleted = false, + Name = "Studio Mic", + Price = 199.99m, + Stock = 200 + }, + new + { + Id = 15, + CategoryId = 6, + IsDeleted = false, + Name = "Webcam", + Price = 215.99m, + Stock = 115 + }, + new + { + Id = 16, + CategoryId = 6, + IsDeleted = false, + Name = "Capture Card", + Price = 708.99m, + Stock = 220 + }, + new + { + Id = 17, + CategoryId = 6, + IsDeleted = false, + Name = "360 Camera", + Price = 4009.99m, + Stock = 115 + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Sales"); + + b.HasData( + new + { + Id = 1, + Date = new DateTime(2026, 4, 30, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + Date = new DateTime(2026, 5, 1, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 3, + Date = new DateTime(2026, 5, 2, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 4, + Date = new DateTime(2026, 5, 3, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 5, + Date = new DateTime(2026, 5, 4, 12, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.SaleProduct", b => + { + b.Property("SalesId") + .HasColumnType("int"); + + b.Property("ProductsId") + .HasColumnType("int"); + + b.Property("Discount") + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("TotalPrice") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("decimal(18,2)") + .HasComputedColumnSql("([Quantity] * [UnitPriceAtSale]) * (1 - [Discount])", true); + + b.Property("UnitPriceAtSale") + .HasColumnType("decimal(18,2)"); + + b.HasKey("SalesId", "ProductsId"); + + b.HasIndex("ProductsId"); + + b.ToTable("SaleProduct"); + + b.HasData( + new + { + SalesId = 1, + ProductsId = 1, + Discount = 0.10m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }, + new + { + SalesId = 2, + ProductsId = 1, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }, + new + { + SalesId = 1, + ProductsId = 3, + Discount = 0.05m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 19.99m + }, + new + { + SalesId = 2, + ProductsId = 4, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 599.99m + }, + new + { + SalesId = 3, + ProductsId = 5, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 3, + ProductsId = 7, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 899.99m + }, + new + { + SalesId = 3, + ProductsId = 8, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 1029.99m + }, + new + { + SalesId = 2, + ProductsId = 5, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 4, + ProductsId = 15, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 215.99m + }, + new + { + SalesId = 4, + ProductsId = 9, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 299.99m + }, + new + { + SalesId = 4, + ProductsId = 8, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 1029.99m + }, + new + { + SalesId = 4, + ProductsId = 5, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 5, + ProductsId = 11, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 489.99m + }, + new + { + SalesId = 5, + ProductsId = 12, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 119.99m + }, + new + { + SalesId = 4, + ProductsId = 17, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 4009.99m + }, + new + { + SalesId = 5, + ProductsId = 1, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.HasOne("EcommerceAPI.Ledana.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.SaleProduct", b => + { + b.HasOne("EcommerceAPI.Ledana.Models.Product", "Product") + .WithMany("SaleProducts") + .HasForeignKey("ProductsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EcommerceAPI.Ledana.Models.Sale", "Sale") + .WithMany("SaleProducts") + .HasForeignKey("SalesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Sale"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Category", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.Navigation("SaleProducts"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Sale", b => + { + b.Navigation("SaleProducts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260516130201_AddedStockInProducts.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260516130201_AddedStockInProducts.cs new file mode 100644 index 00000000..4d11db06 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260516130201_AddedStockInProducts.cs @@ -0,0 +1,148 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EcommerceAPI.Ledana.Migrations +{ + /// + public partial class AddedStockInProducts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Stock", + table: "Products", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 1, + column: "Stock", + value: 115); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 2, + column: "Stock", + value: 0); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 3, + column: "Stock", + value: 0); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 4, + column: "Stock", + value: 89); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 5, + column: "Stock", + value: 115); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 6, + column: "Stock", + value: 120); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 7, + column: "Stock", + value: 115); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 8, + column: "Stock", + value: 250); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 9, + column: "Stock", + value: 115); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 10, + column: "Stock", + value: 200); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 11, + column: "Stock", + value: 115); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 12, + column: "Stock", + value: 170); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 13, + column: "Stock", + value: 115); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 14, + column: "Stock", + value: 200); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 15, + column: "Stock", + value: 115); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 16, + column: "Stock", + value: 220); + + migrationBuilder.UpdateData( + table: "Products", + keyColumn: "Id", + keyValue: 17, + column: "Stock", + value: 115); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Stock", + table: "Products"); + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260517171928_AddSalesView.Designer.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260517171928_AddSalesView.Designer.cs new file mode 100644 index 00000000..ed771121 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260517171928_AddSalesView.Designer.cs @@ -0,0 +1,543 @@ +// +using System; +using EcommerceAPI.Ledana.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EcommerceAPI.Ledana.Migrations +{ + [DbContext(typeof(ProductContext))] + [Migration("20260517171928_AddSalesView")] + partial class AddSalesView + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DeletedOnUtc") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + IsDeleted = false, + Name = "Peripherals" + }, + new + { + Id = 2, + IsDeleted = false, + Name = "Monitors and Displays" + }, + new + { + Id = 3, + IsDeleted = false, + Name = "Computers and Laptops" + }, + new + { + Id = 4, + IsDeleted = false, + Name = "Components" + }, + new + { + Id = 5, + IsDeleted = false, + Name = "Audio and Headsets" + }, + new + { + Id = 6, + IsDeleted = false, + Name = "Video and Cameras" + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("DeletedOnUtc") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Stock") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + IsDeleted = false, + Name = "Wireless Mouse", + Price = 9.99m, + Stock = 115 + }, + new + { + Id = 2, + CategoryId = 1, + IsDeleted = false, + Name = "Nose Cancelling HeadPhones", + Price = 29.99m, + Stock = 0 + }, + new + { + Id = 3, + CategoryId = 1, + IsDeleted = false, + Name = "Silent Keyboard", + Price = 19.99m, + Stock = 0 + }, + new + { + Id = 4, + CategoryId = 2, + IsDeleted = false, + Name = "Curved Monitor", + Price = 599.99m, + Stock = 89 + }, + new + { + Id = 5, + CategoryId = 2, + IsDeleted = false, + Name = "All Surface Projector", + Price = 129.99m, + Stock = 115 + }, + new + { + Id = 6, + CategoryId = 2, + IsDeleted = false, + Name = "Wide 4K Monitor", + Price = 219.99m, + Stock = 120 + }, + new + { + Id = 7, + CategoryId = 3, + IsDeleted = false, + Name = "Gaming Laptop", + Price = 899.99m, + Stock = 115 + }, + new + { + Id = 8, + CategoryId = 3, + IsDeleted = false, + Name = "Workstation PC", + Price = 1029.99m, + Stock = 250 + }, + new + { + Id = 9, + CategoryId = 4, + IsDeleted = false, + Name = "CPU", + Price = 299.99m, + Stock = 115 + }, + new + { + Id = 10, + CategoryId = 4, + IsDeleted = false, + Name = "GPU", + Price = 109.99m, + Stock = 200 + }, + new + { + Id = 11, + CategoryId = 4, + IsDeleted = false, + Name = "RAM", + Price = 489.99m, + Stock = 115 + }, + new + { + Id = 12, + CategoryId = 4, + IsDeleted = false, + Name = "SSD", + Price = 119.99m, + Stock = 170 + }, + new + { + Id = 13, + CategoryId = 5, + IsDeleted = false, + Name = "Gaming Headset", + Price = 89.99m, + Stock = 115 + }, + new + { + Id = 14, + CategoryId = 5, + IsDeleted = false, + Name = "Studio Mic", + Price = 199.99m, + Stock = 200 + }, + new + { + Id = 15, + CategoryId = 6, + IsDeleted = false, + Name = "Webcam", + Price = 215.99m, + Stock = 115 + }, + new + { + Id = 16, + CategoryId = 6, + IsDeleted = false, + Name = "Capture Card", + Price = 708.99m, + Stock = 220 + }, + new + { + Id = 17, + CategoryId = 6, + IsDeleted = false, + Name = "360 Camera", + Price = 4009.99m, + Stock = 115 + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Sales"); + + b.HasData( + new + { + Id = 1, + Date = new DateTime(2026, 4, 30, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + Date = new DateTime(2026, 5, 1, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 3, + Date = new DateTime(2026, 5, 2, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 4, + Date = new DateTime(2026, 5, 3, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 5, + Date = new DateTime(2026, 5, 4, 12, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.SaleProduct", b => + { + b.Property("SalesId") + .HasColumnType("int"); + + b.Property("ProductsId") + .HasColumnType("int"); + + b.Property("Discount") + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("TotalPrice") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("decimal(18,2)") + .HasComputedColumnSql("([Quantity] * [UnitPriceAtSale]) * (1 - [Discount])", true); + + b.Property("UnitPriceAtSale") + .HasColumnType("decimal(18,2)"); + + b.HasKey("SalesId", "ProductsId"); + + b.HasIndex("ProductsId"); + + b.ToTable("SaleProduct"); + + b.HasData( + new + { + SalesId = 1, + ProductsId = 1, + Discount = 0.10m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }, + new + { + SalesId = 2, + ProductsId = 1, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }, + new + { + SalesId = 1, + ProductsId = 3, + Discount = 0.05m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 19.99m + }, + new + { + SalesId = 2, + ProductsId = 4, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 599.99m + }, + new + { + SalesId = 3, + ProductsId = 5, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 3, + ProductsId = 7, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 899.99m + }, + new + { + SalesId = 3, + ProductsId = 8, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 1029.99m + }, + new + { + SalesId = 2, + ProductsId = 5, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 4, + ProductsId = 15, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 215.99m + }, + new + { + SalesId = 4, + ProductsId = 9, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 299.99m + }, + new + { + SalesId = 4, + ProductsId = 8, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 1029.99m + }, + new + { + SalesId = 4, + ProductsId = 5, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 5, + ProductsId = 11, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 489.99m + }, + new + { + SalesId = 5, + ProductsId = 12, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 119.99m + }, + new + { + SalesId = 4, + ProductsId = 17, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 4009.99m + }, + new + { + SalesId = 5, + ProductsId = 1, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.HasOne("EcommerceAPI.Ledana.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.SaleProduct", b => + { + b.HasOne("EcommerceAPI.Ledana.Models.Product", "Product") + .WithMany("SaleProducts") + .HasForeignKey("ProductsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EcommerceAPI.Ledana.Models.Sale", "Sale") + .WithMany("SaleProducts") + .HasForeignKey("SalesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Sale"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Category", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.Navigation("SaleProducts"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Sale", b => + { + b.Navigation("SaleProducts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260517171928_AddSalesView.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260517171928_AddSalesView.cs new file mode 100644 index 00000000..9a776761 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/20260517171928_AddSalesView.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EcommerceAPI.Ledana.Migrations +{ + /// + public partial class AddSalesView : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" + CREATE VIEW v_SalesWithTotal AS + SELECT s.Id, + s.Date, + COALESCE(SUM(sp.TotalPrice), 0) AS TotalPrice + FROM Sales s + LEFT JOIN SaleProduct sp ON sp.SalesId = s.Id + GROUP BY s.Id, s.Date; +"); + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DROP VIEW v_SalesWithTotal;"); + + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/ProductContextModelSnapshot.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/ProductContextModelSnapshot.cs new file mode 100644 index 00000000..bfb7edcd --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Migrations/ProductContextModelSnapshot.cs @@ -0,0 +1,540 @@ +// +using System; +using EcommerceAPI.Ledana.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EcommerceAPI.Ledana.Migrations +{ + [DbContext(typeof(ProductContext))] + partial class ProductContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DeletedOnUtc") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = 1, + IsDeleted = false, + Name = "Peripherals" + }, + new + { + Id = 2, + IsDeleted = false, + Name = "Monitors and Displays" + }, + new + { + Id = 3, + IsDeleted = false, + Name = "Computers and Laptops" + }, + new + { + Id = 4, + IsDeleted = false, + Name = "Components" + }, + new + { + Id = 5, + IsDeleted = false, + Name = "Audio and Headsets" + }, + new + { + Id = 6, + IsDeleted = false, + Name = "Video and Cameras" + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("DeletedOnUtc") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Stock") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + + b.HasData( + new + { + Id = 1, + CategoryId = 1, + IsDeleted = false, + Name = "Wireless Mouse", + Price = 9.99m, + Stock = 115 + }, + new + { + Id = 2, + CategoryId = 1, + IsDeleted = false, + Name = "Nose Cancelling HeadPhones", + Price = 29.99m, + Stock = 0 + }, + new + { + Id = 3, + CategoryId = 1, + IsDeleted = false, + Name = "Silent Keyboard", + Price = 19.99m, + Stock = 0 + }, + new + { + Id = 4, + CategoryId = 2, + IsDeleted = false, + Name = "Curved Monitor", + Price = 599.99m, + Stock = 89 + }, + new + { + Id = 5, + CategoryId = 2, + IsDeleted = false, + Name = "All Surface Projector", + Price = 129.99m, + Stock = 115 + }, + new + { + Id = 6, + CategoryId = 2, + IsDeleted = false, + Name = "Wide 4K Monitor", + Price = 219.99m, + Stock = 120 + }, + new + { + Id = 7, + CategoryId = 3, + IsDeleted = false, + Name = "Gaming Laptop", + Price = 899.99m, + Stock = 115 + }, + new + { + Id = 8, + CategoryId = 3, + IsDeleted = false, + Name = "Workstation PC", + Price = 1029.99m, + Stock = 250 + }, + new + { + Id = 9, + CategoryId = 4, + IsDeleted = false, + Name = "CPU", + Price = 299.99m, + Stock = 115 + }, + new + { + Id = 10, + CategoryId = 4, + IsDeleted = false, + Name = "GPU", + Price = 109.99m, + Stock = 200 + }, + new + { + Id = 11, + CategoryId = 4, + IsDeleted = false, + Name = "RAM", + Price = 489.99m, + Stock = 115 + }, + new + { + Id = 12, + CategoryId = 4, + IsDeleted = false, + Name = "SSD", + Price = 119.99m, + Stock = 170 + }, + new + { + Id = 13, + CategoryId = 5, + IsDeleted = false, + Name = "Gaming Headset", + Price = 89.99m, + Stock = 115 + }, + new + { + Id = 14, + CategoryId = 5, + IsDeleted = false, + Name = "Studio Mic", + Price = 199.99m, + Stock = 200 + }, + new + { + Id = 15, + CategoryId = 6, + IsDeleted = false, + Name = "Webcam", + Price = 215.99m, + Stock = 115 + }, + new + { + Id = 16, + CategoryId = 6, + IsDeleted = false, + Name = "Capture Card", + Price = 708.99m, + Stock = 220 + }, + new + { + Id = 17, + CategoryId = 6, + IsDeleted = false, + Name = "360 Camera", + Price = 4009.99m, + Stock = 115 + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Sales"); + + b.HasData( + new + { + Id = 1, + Date = new DateTime(2026, 4, 30, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + Date = new DateTime(2026, 5, 1, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 3, + Date = new DateTime(2026, 5, 2, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 4, + Date = new DateTime(2026, 5, 3, 12, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 5, + Date = new DateTime(2026, 5, 4, 12, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.SaleProduct", b => + { + b.Property("SalesId") + .HasColumnType("int"); + + b.Property("ProductsId") + .HasColumnType("int"); + + b.Property("Discount") + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("TotalPrice") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("decimal(18,2)") + .HasComputedColumnSql("([Quantity] * [UnitPriceAtSale]) * (1 - [Discount])", true); + + b.Property("UnitPriceAtSale") + .HasColumnType("decimal(18,2)"); + + b.HasKey("SalesId", "ProductsId"); + + b.HasIndex("ProductsId"); + + b.ToTable("SaleProduct"); + + b.HasData( + new + { + SalesId = 1, + ProductsId = 1, + Discount = 0.10m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }, + new + { + SalesId = 2, + ProductsId = 1, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }, + new + { + SalesId = 1, + ProductsId = 3, + Discount = 0.05m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 19.99m + }, + new + { + SalesId = 2, + ProductsId = 4, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 599.99m + }, + new + { + SalesId = 3, + ProductsId = 5, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 3, + ProductsId = 7, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 899.99m + }, + new + { + SalesId = 3, + ProductsId = 8, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 1029.99m + }, + new + { + SalesId = 2, + ProductsId = 5, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 4, + ProductsId = 15, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 215.99m + }, + new + { + SalesId = 4, + ProductsId = 9, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 299.99m + }, + new + { + SalesId = 4, + ProductsId = 8, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 1029.99m + }, + new + { + SalesId = 4, + ProductsId = 5, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 129.99m + }, + new + { + SalesId = 5, + ProductsId = 11, + Discount = 0.10m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 489.99m + }, + new + { + SalesId = 5, + ProductsId = 12, + Discount = 0m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 119.99m + }, + new + { + SalesId = 4, + ProductsId = 17, + Discount = 0.05m, + Quantity = 1, + TotalPrice = 0m, + UnitPriceAtSale = 4009.99m + }, + new + { + SalesId = 5, + ProductsId = 1, + Discount = 0.20m, + Quantity = 2, + TotalPrice = 0m, + UnitPriceAtSale = 9.99m + }); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.HasOne("EcommerceAPI.Ledana.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.SaleProduct", b => + { + b.HasOne("EcommerceAPI.Ledana.Models.Product", "Product") + .WithMany("SaleProducts") + .HasForeignKey("ProductsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EcommerceAPI.Ledana.Models.Sale", "Sale") + .WithMany("SaleProducts") + .HasForeignKey("SalesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Sale"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Category", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Product", b => + { + b.Navigation("SaleProducts"); + }); + + modelBuilder.Entity("EcommerceAPI.Ledana.Models.Sale", b => + { + b.Navigation("SaleProducts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Models/Category.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Models/Category.cs new file mode 100644 index 00000000..b66fdb71 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Models/Category.cs @@ -0,0 +1,13 @@ +using EcommerceAPI.Ledana.Interfaces; + +namespace EcommerceAPI.Ledana.Models +{ + public class Category : ISoftDeletable + { + public int Id { get; set; } + public string Name { get; set; } = null!; + public List Products { get; set; } = []; + public bool IsDeleted { get; set; } + public DateTime? DeletedOnUtc { get; set; } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Models/Product.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Models/Product.cs new file mode 100644 index 00000000..fd987fbf --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Models/Product.cs @@ -0,0 +1,18 @@ +using EcommerceAPI.Ledana.Interfaces; + +namespace EcommerceAPI.Ledana.Models +{ + public class Product : ISoftDeletable + { + public int Id { get; set; } + public string Name { get; set; } = null!; + public decimal Price { get; set; } + public int Stock { get; set; } + public int CategoryId { get; set; } + public Category Category { get; set; } = null!; + public List Sales { get; set; } = []; + public List SaleProducts { get; set; } = []; + public bool IsDeleted { get; set; } + public DateTime? DeletedOnUtc { get; set; } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Models/Sale.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Models/Sale.cs new file mode 100644 index 00000000..2340ddfa --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Models/Sale.cs @@ -0,0 +1,22 @@ +namespace EcommerceAPI.Ledana.Models +{ + public class Sale + { + public int Id { get; set; } + + public DateTime Date { get; set; } + public List Products { get; set; } = []; + + public List SaleProducts { get; set; } = []; + + } + + //tried creating a view in db to have totalprice for each sale + public class SaleWithTotal + { + public int Id { get; set; } + + public DateTime Date { get; set; } + public decimal TotalPrice { get; set; } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Models/SaleProduct.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Models/SaleProduct.cs new file mode 100644 index 00000000..c29add1e --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Models/SaleProduct.cs @@ -0,0 +1,15 @@ +namespace EcommerceAPI.Ledana.Models +{ + public class SaleProduct + { + public int ProductsId { get; set; } + public Product Product { get; set; } = null!; + public int SalesId { get; set; } + public Sale Sale { get; set; } = null!; + public int Quantity { get; set; } + public decimal Discount { get; set; } + public decimal UnitPriceAtSale { get; set; } + public decimal TotalPrice { get; set; } + + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Options/ProductOptions.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Options/ProductOptions.cs new file mode 100644 index 00000000..d6c52b09 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Options/ProductOptions.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc; + +namespace EcommerceAPI.Ledana.Options +{ + public class ProductOptions + { + [FromQuery(Name = "name")] + public string? Name { get; set; } + [FromQuery(Name = "category")] + public string? Category { get; set; } + [FromQuery(Name = "stock")] + public int? Stock { get; set; } + [FromQuery(Name = "price")] + public decimal? Price { get; set; } + [FromQuery(Name = "page_number")] + public int PageNumber { get; set; } = 1; + [FromQuery(Name = "page_size")] + public int PageSize { get; set; } = 100; + [FromQuery(Name = "sort_by")] + public string SortBy { get; set; } = "id"; + [FromQuery(Name = "sort_order")] + public string SortOrder { get; set; } = "ASC"; + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Options/SaleOptions.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Options/SaleOptions.cs new file mode 100644 index 00000000..24628bf2 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Options/SaleOptions.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc; + +namespace EcommerceAPI.Ledana.Options +{ + public class SaleOptions + { + [FromQuery(Name = "product_name")] + public string? ProductName { get; set; } + [FromQuery(Name = "category_name")] + public string? CategoryName { get; set; } + [FromQuery(Name = "total_price")] + public decimal? TotalPrice { get; set; } + [FromQuery(Name = "date")] + public DateTime? Date { get; set; } + [FromQuery(Name = "page_number")] + public int PageNumber { get; set; } = 1; + [FromQuery(Name = "page_size")] + public int PageSize { get; set; } = 100; + [FromQuery(Name = "sort_by")] + public string SortBy { get; set; } = "id"; + [FromQuery(Name = "sort_order")] + public string SortOrder { get; set; } = "ASC"; + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Program.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Program.cs new file mode 100644 index 00000000..bd418307 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Program.cs @@ -0,0 +1,51 @@ +using AutoMapper; +using EcommerceAPI.Ledana.Data; +using EcommerceAPI.Ledana.Interfaces; +using EcommerceAPI.Ledana.Services; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +//adding the dbcontext +var connectionString = builder.Configuration.GetConnectionString("ECommerceDb") + ?? throw new InvalidOperationException("Connection string 'ECommerceDb' not found!"); + +builder.Services.AddDbContext(options => + options.UseSqlServer(connectionString)); + +//adding the product service +builder.Services.AddScoped(); +//adding the category service +builder.Services.AddScoped(); + +//adding the sale service +builder.Services.AddScoped(); + +//configuring json serializer to handle cycles +builder.Services.AddControllers().AddJsonOptions(opt => + opt.JsonSerializerOptions.ReferenceHandler = + System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles); + +//add the mapper +builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + + +builder.Services.AddControllers(); + +//adding swagger +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.MapControllers(); + +app.Run(); diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Properties/launchSettings.json b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Properties/launchSettings.json new file mode 100644 index 00000000..1e55dd18 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/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:5128", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7077;http://localhost:5128", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Services/CategoryService.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Services/CategoryService.cs new file mode 100644 index 00000000..03d859af --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Services/CategoryService.cs @@ -0,0 +1,139 @@ +using AutoMapper; +using Azure; +using EcommerceAPI.Ledana.Data; +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Interfaces; +using EcommerceAPI.Ledana.Models; +using Microsoft.EntityFrameworkCore; +using System.Net; + +namespace EcommerceAPI.Ledana.Services +{ + public class CategoryService : ICategoryService + { + private readonly ProductContext _dbContext; + private readonly IMapper _mapper; + + public CategoryService(ProductContext productContext, IMapper mapper) + { + _dbContext = productContext; + _mapper = mapper; + } + + public async Task>> GetAllCategories() + { + var categories = await _dbContext.Categories.Include(c => c.Products) + .ToListAsync(); + + if (categories is null) + return new ApiResponseDto> + { + RequestFailed = true, + ResponseCode = HttpStatusCode.BadRequest, + ErrorMessage = "Couldn't get categories", + Data = null + }; + + return new ApiResponseDto> + { + RequestFailed = false, + ResponseCode = HttpStatusCode.OK, + Data = categories + }; + } + + public async Task> GetCategoryById(int id) + { + var category = await _dbContext.Categories.Include(c => c.Products).FirstAsync(c => c.Id == id); + + if (category is null) return new ApiResponseDto + { + RequestFailed = true, + ResponseCode = HttpStatusCode.NotFound, + Data = null, + ErrorMessage = "Couldn't find the category" + }; + + return new ApiResponseDto + { + RequestFailed = false, + ResponseCode = HttpStatusCode.OK, + Data = category + }; + } + public async Task> CreateCategory(CategoryDto category) + { + var newCategory = _mapper.Map(category); + + var response = await _dbContext.Categories.AddAsync(newCategory); + await _dbContext.SaveChangesAsync(); + + return new() + { + Data = response.Entity, + ResponseCode = HttpStatusCode.Created + }; + } + + public async Task> UpdateCategory(int id, CategoryDto category) + { + var existingCategory = await _dbContext.Categories.FirstOrDefaultAsync(c => c.Id == id); + + if (existingCategory is null) + return new() + { + RequestFailed = true, + Data = null, + ResponseCode = HttpStatusCode.NotFound, + ErrorMessage = "Couldn't find the category" + }; + + existingCategory = _mapper.Map(category, existingCategory); + + await _dbContext.SaveChangesAsync(); + + return new() + { + Data = existingCategory, + ResponseCode = HttpStatusCode.OK + }; + } + + public async Task> SoftDeleteCategory(int id) + { + var category = await _dbContext.Categories + .Include(c => c.Products) + .FirstOrDefaultAsync(c => c.Id == id); + + + if (category is null) + { + return new ApiResponseDto + { + RequestFailed = true, + Data = null, + ResponseCode = HttpStatusCode.NotFound, + ErrorMessage = $"Resource with id {id} was not found" + }; + } + + category.IsDeleted = true; + category.DeletedOnUtc = DateTime.UtcNow; + + foreach (var item in category.Products) + { + item.IsDeleted = true; + item.DeletedOnUtc = DateTime.UtcNow; + } + + await _dbContext.SaveChangesAsync(); + + return new ApiResponseDto + { + Data = $"Category with id {id} is deleted!", + ResponseCode = HttpStatusCode.NoContent + }; + } + + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Services/ProductService.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Services/ProductService.cs new file mode 100644 index 00000000..2571f79a --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Services/ProductService.cs @@ -0,0 +1,195 @@ +using AutoMapper; +using EcommerceAPI.Ledana.Data; +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Interfaces; +using EcommerceAPI.Ledana.Models; +using EcommerceAPI.Ledana.Options; +using Microsoft.EntityFrameworkCore; +using System.Net; + +namespace EcommerceAPI.Ledana.Services +{ + public class ProductService : IProductService + { + private readonly ProductContext _dbContext; + private readonly IMapper _mapper; + + public ProductService(ProductContext productContext, IMapper mapper) + { + _dbContext = productContext; + _mapper = mapper; + } + + public async Task?>> GetAllProducts(ProductOptions productOptions) + { + //when calling get product with pagination the user can view, update or delete products. + //so they will see even products with stock equal to 0 + var query = _dbContext.Products + .Include(p => p.Category) + .AsQueryable(); + + var totalProducts = await query.CountAsync(); + List? products; + + if (productOptions.Name is not null) + query = query.Where(p => p.Name.ToLower() == productOptions.Name.ToLower()); + if (productOptions.Category is not null) + query = query.Where(p => p.Category.Name.ToLower() == productOptions.Category.ToLower()); + if (productOptions.Price is not null) + query = query.Where(p => p.Price <= productOptions.Price); + if (productOptions.Stock is not null) + query = query.Where(p => p.Stock <= productOptions.Stock); + + if(productOptions.SortBy == "id" || !string.IsNullOrEmpty(productOptions.SortBy)) + { + switch (productOptions.SortBy) + { + case "name": + query = productOptions.SortOrder.ToUpper() == "ASC" + ? query.OrderBy(p => p.Name) + : query.OrderByDescending(p => p.Name); + break; + case "category": + query = productOptions.SortOrder.ToUpper() == "ASC" + ? query.OrderBy(p => p.Category.Name) + : query.OrderByDescending(p => p.Category.Name); + break; + case "price": + query = productOptions.SortOrder.ToUpper() == "ASC" + ? query.OrderBy(p => p.Price) + : query.OrderByDescending(p => p.Price); + break; + case "stock": + query = productOptions.SortOrder.ToUpper() == "ASC" + ? query.OrderBy(p => p.Stock) + : query.OrderByDescending(p => p.Stock); + break; + default: + query = productOptions.SortOrder.ToUpper() == "ASC" + ? query.OrderBy(p => p.Id) + : query.OrderByDescending(p => p.Id); + break; + } + } + + query = query.Skip((productOptions.PageNumber - 1) * productOptions.PageSize) + .Take(productOptions.PageSize); + products = await query.ToListAsync(); + bool hasPrevious = productOptions.PageNumber > 1; + bool hasNext = (productOptions.PageNumber * productOptions.PageSize) < totalProducts; + + + return new ApiResponseDto?> + { + ResponseCode = HttpStatusCode.OK, + Data = products, + TotalCount = totalProducts, + CurrentPage = productOptions.PageNumber, + PageSize = productOptions.PageSize, + HasPrevious = hasPrevious, + HasNext = hasNext + }; + } + public async Task?>> GetAllProducts() + { + //when calling get products without pagination user can add these products to a new sale + //so they will see only products with stock greater then zero + var products = await _dbContext.Products.Where(p => p.Stock > 0) + .Include(p => p.Category).ToListAsync(); + if (products is null) return new() + { + RequestFailed = true, + Data = null, + ErrorMessage = "Could not get products", + ResponseCode = HttpStatusCode.BadRequest + }; + return new ApiResponseDto?> + { + ResponseCode = HttpStatusCode.OK, + Data = products + }; + } + public async Task> GetProductById(int id) + { + var product = await _dbContext.Products.Include(p => p.Category).FirstOrDefaultAsync(p => p.Id == id); + if (product is null) return new ApiResponseDto + { + RequestFailed = true, + ErrorMessage = "Couldn't find the product", + ResponseCode = HttpStatusCode.NotFound + }; + + return new ApiResponseDto + { + ResponseCode = HttpStatusCode.OK, + Data = product + }; + } + + public async Task> CreateProduct(ProductDto product) + { + Product newProduct = _mapper.Map(product); + + var response = await _dbContext.Products.AddAsync(newProduct); + await _dbContext.SaveChangesAsync(); + + return new() + { + Data = response.Entity, + ResponseCode = HttpStatusCode.Created + }; + } + + public async Task?> UpdateProduct(int id, ProductUpdateDto product) + { + Product? existingProduct = await _dbContext.Products.FirstOrDefaultAsync(p => p.Id == id); + if (existingProduct is null) + return new ApiResponseDto() + { + RequestFailed = true, + ErrorMessage = "Couldn't find product", + ResponseCode = HttpStatusCode.NotFound, + Data = null + }; + + existingProduct = _mapper.Map(product, existingProduct); + + await _dbContext.SaveChangesAsync(); + + return new() + { + ResponseCode = HttpStatusCode.OK, + Data = existingProduct + }; + } + + public async Task> SoftDeleteProduct(int id) + { + var product = await _dbContext.Products.FindAsync(id); + + if (product is null) + { + return new ApiResponseDto + { + RequestFailed = true, + Data = null, + ResponseCode = HttpStatusCode.NotFound, + ErrorMessage = $"Resource with id {id} was not found" + }; + } + + product.IsDeleted = true; + product.DeletedOnUtc = DateTime.UtcNow; + + await _dbContext.SaveChangesAsync(); + + return new ApiResponseDto + { + Data = $"Product with id {id} is deleted!", + ResponseCode = HttpStatusCode.NoContent + }; + } + + + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Services/SaleService.cs b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Services/SaleService.cs new file mode 100644 index 00000000..33792d1d --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/Services/SaleService.cs @@ -0,0 +1,235 @@ +using AutoMapper; +using EcommerceAPI.Ledana.Data; +using EcommerceAPI.Ledana.DTOs; +using EcommerceAPI.Ledana.Interfaces; +using EcommerceAPI.Ledana.Models; +using EcommerceAPI.Ledana.Options; +using Microsoft.EntityFrameworkCore; +using System.Net; + +namespace EcommerceAPI.Ledana.Services +{ + public class SaleService : ISaleService + { + private readonly IMapper _mapper; + public readonly ProductContext _dbContext; + + public SaleService(IMapper mapper, ProductContext dbContext) + { + _mapper = mapper; + _dbContext = dbContext; + } + + public async Task> CreateSale(SaleDto sale) + { + + var newSale = _mapper.Map(sale); + + + foreach (var item in newSale.SaleProducts) + { + var product = await _dbContext.Products.FindAsync(item.ProductsId); + + if (product is null) return new() + { + RequestFailed = true, + ErrorMessage = $"Product with ID {item.ProductsId} not found", + ResponseCode = HttpStatusCode.BadRequest + }; + + //saving for each product added in sale its price to unit price at sale for saleproduct + item.UnitPriceAtSale = product.Price; + + //checking if quanity is bigger then stock + if (item.Quantity > product.Stock) + return new() + { + RequestFailed = true, + ErrorMessage = "Amount is bigger than stock", + ResponseCode = HttpStatusCode.BadRequest + }; + //removing the quantity of products bought from stock of product + product.Stock -= item.Quantity; + } + + var response = await _dbContext.Sales + .AddAsync(newSale); + + if (response is null) return new() + { + RequestFailed = true, + ErrorMessage = "Couldn't add the new sale", + Data = null, + ResponseCode = HttpStatusCode.BadRequest + }; + + await _dbContext.SaveChangesAsync(); + return new() + { + Data = response.Entity, + ResponseCode = HttpStatusCode.OK + }; + } + public async Task>> GetAllSales() + { + var query = _dbContext.Sales + .IgnoreQueryFilters() + .Include(s => s.SaleProducts).ThenInclude(sp => sp.Product) + .ThenInclude(p => p.Category) + .Select(s => new SaleProductViewDto + { + SaleId = s.Id, + Date = s.Date, + TotalPrice = s.SaleProducts.Sum(sp => sp.TotalPrice), + Products = s.SaleProducts.Select(sp => new SaleProductListDto + { + ProductName = sp.Product.Name, + CategoryName = sp.Product.Category.Name, + Quantity = sp.Quantity, + UnitPriceAtSale = sp.UnitPriceAtSale, + Discount = sp.Discount, + TotalPrice = sp.TotalPrice + }).ToList() + }) + .AsQueryable(); + + List? sales; + + sales = await query.ToListAsync(); + + if (sales is null) return new() + { + RequestFailed = true, + ErrorMessage = "Couldn't fetch the sales", + Data = null, + ResponseCode = HttpStatusCode.BadRequest + }; + + return new ApiResponseDto>() + { + Data = sales, + ResponseCode = HttpStatusCode.OK + }; + } + public async Task>> GetAllSales(SaleOptions saleOptions) + { + var query = _dbContext.Sales + .IgnoreQueryFilters() + .Include(s => s.SaleProducts).ThenInclude(sp => sp.Product) + .ThenInclude(p => p.Category) + .Select(s => new SaleProductViewDto + { + SaleId = s.Id, + Date = s.Date, + TotalPrice = s.SaleProducts.Sum(sp => sp.TotalPrice), + Products = s.SaleProducts.Select(sp => new SaleProductListDto + { + ProductName = sp.Product.Name, + CategoryName = sp.Product.Category.Name, + Quantity = sp.Quantity, + UnitPriceAtSale = sp.UnitPriceAtSale, + Discount = sp.Discount, + TotalPrice = sp.TotalPrice + }).ToList() + }) + .AsQueryable(); + + var totalSales = await query.CountAsync(); + List? sales; + + if (saleOptions.ProductName is not null) + query = query.Where(s => s.Products.Any(p => p.ProductName.ToLower() == saleOptions.ProductName.ToLower())); + if (saleOptions.CategoryName is not null) + query = query.Where(s => s.Products.Any(p => p.CategoryName.ToLower() == saleOptions.CategoryName.ToLower())); + if (saleOptions.TotalPrice is not null) + query = query.Where(s => s.TotalPrice <= saleOptions.TotalPrice); + if (saleOptions.Date.HasValue) + query = query.Where(s => s.Date <= saleOptions.Date); + + if (saleOptions.SortBy == "id" || !string.IsNullOrEmpty(saleOptions.SortBy)) + { + switch (saleOptions.SortBy) + { + + case "total_price": + query = saleOptions.SortOrder.ToUpper() == "ASC" + ? query.OrderBy(s => s.TotalPrice) + : query.OrderByDescending(s => s.TotalPrice); + break; + case "date": + query = saleOptions.SortOrder.ToUpper() == "ASC" + ? query.OrderBy(s => s.Date) + : query.OrderByDescending(s => s.Date); + break; + default: + query = saleOptions.SortOrder.ToUpper() == "ASC" + ? query.OrderBy(s => s.SaleId) + : query.OrderByDescending(s => s.SaleId); + break; + } + } + + query = query.Skip((saleOptions.PageNumber - 1) * saleOptions.PageSize) + .Take(saleOptions.PageSize); + sales = await query.ToListAsync(); + bool hasPrevious = saleOptions.PageNumber > 1; + bool hasNext = (saleOptions.PageNumber * saleOptions.PageSize) < totalSales; + + if (sales is null) return new() + { + RequestFailed = true, + ErrorMessage = "Couldn't fetch the sales", + Data = null, + ResponseCode = HttpStatusCode.BadRequest + }; + + return new ApiResponseDto>() + { + Data = sales, + ResponseCode = HttpStatusCode.OK, + TotalCount = totalSales, + CurrentPage = saleOptions.PageNumber, + PageSize = saleOptions.PageSize, + HasPrevious = hasPrevious, + HasNext = hasNext + }; + } + + public async Task> GetSaleById(int id) + { + var sale = await _dbContext.Sales.IgnoreQueryFilters() + .Include(s => s.SaleProducts) + .ThenInclude(sp => sp.Product) + .ThenInclude(p => p.Category) + .Select(s => new SaleProductViewDto + { + SaleId = s.Id, + Date = s.Date, + TotalPrice = s.SaleProducts.Sum(sp => sp.TotalPrice), + Products = s.SaleProducts.Select(sp => new SaleProductListDto + { + ProductName = sp.Product.Name, + CategoryName = sp.Product.Category.Name, + Quantity = sp.Quantity, + UnitPriceAtSale = sp.UnitPriceAtSale, + Discount = sp.Discount, + TotalPrice = sp.TotalPrice + }).ToList() + }).FirstAsync(s => s.SaleId == id); ; + + if (sale is null) return new() + { + RequestFailed = true, + Data = null, + ErrorMessage = "Couldn't find the sale", + ResponseCode = HttpStatusCode.NotFound + }; + + return new() + { + Data = sale, + ResponseCode = HttpStatusCode.OK + }; + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/appsettings.Development.json b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/EcommerceAPI.Ledana/EcommerceAPI.Ledana/appsettings.json b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/appsettings.json new file mode 100644 index 00000000..b1e118e2 --- /dev/null +++ b/EcommerceAPI.Ledana/EcommerceAPI.Ledana/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "ECommerceDb": "Data Source=(localDb)\\MSSQLLocalDB;Initial Catalog=ECommerceDb; Trusted_Connection=True" + }, + "AllowedHosts": "*" +} diff --git a/EcommerceAPI.Ledana/LICENSE b/EcommerceAPI.Ledana/LICENSE new file mode 100644 index 00000000..a8a795d3 --- /dev/null +++ b/EcommerceAPI.Ledana/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 ledana gjoka + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/EcommerceAPI.Ledana/README.md b/EcommerceAPI.Ledana/README.md new file mode 100644 index 00000000..08fab381 --- /dev/null +++ b/EcommerceAPI.Ledana/README.md @@ -0,0 +1,164 @@ +# 🛒 E-Commerce API + +A backend e-commerce application built with **ASP.NET Core Web API** following **Clean Architecture** principles. +The project focuses on scalable backend development, relational database design, and RESTful API practices using modern .NET technologies. + +--- + +# 🚀 Features + +* RESTful ASP.NET Core Web API +* Clean Architecture structure +* CRUD operations for Products, Categories, and Sales +* Many-to-many relationship handling +* Entity Framework Core with SQL Server +* DTOs and AutoMapper mapping +* Server-side Pagination +* Filtering & Sorting +* Dependency Injection +* Async/Await operations +* Model Validation +* Global Exception Handling +* Console Client consuming API endpoints using HttpClient +* Git & Version Control workflow + +--- + +# 🛠 Tech Stack + +## Backend + +* C# +* ASP.NET Core Web API +* Entity Framework Core +* SQL Server +* LINQ +* HttpClient + +## Architecture & Concepts + +* Clean Architecture +* Repository & Service Patterns +* SOLID Principles +* RESTful APIs +* DTOs +* Dependency Injection +* Async Programming + +## Tools + +* Git +* GitHub +* Swagger/OpenAPI +* Visual Studio + +--- + +# 📦 Main Functionalities + +## Products + +* Create products +* Update products +* Delete products +* Retrieve products +* Pagination, filtering & sorting + +## Categories + +* Category management +* Product-category relationships + +## Sales + +* Sales tracking +* Product-sale relationships + +--- + +# 🗄 Database + +The application uses **SQL Server** with **Entity Framework Core** migrations. + +Features include: + +* Relational database design +* Many-to-many relationships +* Fluent API configurations +* Code-first migrations + +--- + +# 🔄 API Consumption + +A separate console client application consumes the API using: + +* HttpClient +* JSON serialization/deserialization +* Asynchronous HTTP requests + +This demonstrates a decoupled client-server architecture. + +--- + +# ▶️ Getting Started + +## Clone the Repository + +```bash +git clone https://github.com/Ledana/EcommerceAPI.Ledana.git +``` + +## Navigate to the Project + +```bash +cd EcommerceAPI.Ledana +``` + +## Update Database + +```bash +update-database +``` + +## Run the API + +```bash +dotnet run +``` + +--- + +# 📘 Learning Goals + +This project was built to strengthen knowledge in: + +* ASP.NET Core Web API development +* Clean Architecture +* EF Core & SQL Server +* Authentication & Authorization +* REST API design +* Layered application structure +* Backend best practices + +--- + +# 📌 Future Improvements + +* ASP.NET Core MVC frontend +* Docker containerization +* Azure deployment +* Integration testing +* Refresh tokens +* Logging & monitoring +* Unit & integration test coverage + +--- + +# 👩‍💻 Author + +## [Ledana Gjoka GitHub](https://github.com/Ledana) + +Junior Backend .NET Developer passionate about backend development, clean code, and scalable application design. + +Repository: [EcommerceAPI.Ledana](https://github.com/Ledana/EcommerceAPI.Ledana.git)