From 4a087a6ff7cb9d7c72e0b27d21c3669e95efba35 Mon Sep 17 00:00:00 2001 From: DSills735 Date: Wed, 10 Jun 2026 08:34:28 -0500 Subject: [PATCH] project finished. --- .gitattributes | 63 +++ README.md | 66 ++++ eCommerceApi.dsills735/.gitattributes | 63 +++ eCommerceApi.dsills735/.gitignore | 363 ++++++++++++++++++ .../Sills.GolfShop.eCommerce.slnx | 3 + .../Controllers/CategoryController.cs | 73 ++++ .../Controllers/ProductController.cs | 73 ++++ .../Controllers/SalesController.cs | 61 +++ .../Controllers/SeedController.cs | 54 +++ .../DTO/BulkSeedingDto.cs | 7 + .../DTO/ProductUpdateDTO.cs | 3 + .../DTO/SeedingDto.cs | 5 + .../Data/GolfShopDbContext.cs | 24 ++ .../Document_Processing/Reader.cs | 223 +++++++++++ .../Document_Processing/ReportGenerator.cs | 124 ++++++ .../Document_Processing/ResourceDebugger.cs | 56 +++ .../Helpers/PaginationParameters.cs | 14 + .../Helpers/SortingParameters.cs | 20 + .../Mapping/ProductMapper.cs | 15 + .../20260403232616_initial.Designer.cs | 152 ++++++++ .../Migrations/20260403232616_initial.cs | 107 ++++++ .../GolfShopDbContextModelSnapshot.cs | 149 +++++++ .../Models/Categories.cs | 11 + .../Models/Product.cs | 14 + .../Models/ProductSales.cs | 13 + .../Sills.GolfShop.eCommerce/Models/Sales.cs | 11 + .../Postman_Collection/PostmanCollection.json | 241 ++++++++++++ .../Sills.GolfShop.eCommerce/Program.cs | 173 +++++++++ .../Properties/launchSettings.json | 23 ++ .../Resources/DataToSeed.csv | 141 +++++++ .../Services/ICategoryService.cs | 80 ++++ .../Services/IProductsService.cs | 83 ++++ .../Services/ISalesService.cs | 100 +++++ .../Sills.GolfShop.eCommerce.http | 0 .../Sills.GolfShop.eCommerceAPI.csproj | 29 ++ .../appsettings.Development.json | 8 + .../Sills.GolfShop.eCommerce/appsettings.json | 13 + 37 files changed, 2658 insertions(+) create mode 100644 .gitattributes create mode 100644 README.md create mode 100644 eCommerceApi.dsills735/.gitattributes create mode 100644 eCommerceApi.dsills735/.gitignore create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce.slnx create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/CategoryController.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/ProductController.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/SalesController.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/SeedController.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/DTO/BulkSeedingDto.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/DTO/ProductUpdateDTO.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/DTO/SeedingDto.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Data/GolfShopDbContext.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Document_Processing/Reader.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Document_Processing/ReportGenerator.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Document_Processing/ResourceDebugger.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Helpers/PaginationParameters.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Helpers/SortingParameters.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Mapping/ProductMapper.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Migrations/20260403232616_initial.Designer.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Migrations/20260403232616_initial.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Migrations/GolfShopDbContextModelSnapshot.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/Categories.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/Product.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/ProductSales.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/Sales.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Postman_Collection/PostmanCollection.json create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Program.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Properties/launchSettings.json create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Resources/DataToSeed.csv create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Services/ICategoryService.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Services/IProductsService.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Services/ISalesService.cs create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Sills.GolfShop.eCommerce.http create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Sills.GolfShop.eCommerceAPI.csproj create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/appsettings.Development.json create mode 100644 eCommerceApi.dsills735/Sills.GolfShop.eCommerce/appsettings.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.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/README.md b/README.md new file mode 100644 index 0000000..5c29b1f --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Golf Shop eCommerce API + +A .NET 10 REST API for managing a golf shop inventory with automatic database seeding from spreadsheets (CSV, XLSX, XLS). + +## Features + +- Auto-seed database on first run +- Support for CSV, Excel (.xlsx, .xls) formats +- RESTful API with Swagger documentation +- PDF report generation +- Entity Framework Core with SQL Server + +## Prerequisites + +- .NET 10 SDK +- SQL Server LocalDB +- Visual Studio 2022+ (optional) + +## Setup + +1. Update connection string in `appsettings.json`: +```json +"ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=GolfShopDb;Trusted_Connection=True;MultipleActiveResultSets=true" +} +``` + +2. Add seed data file to `Resources/DataToSeed.csv` (or .xlsx/.xls) +3. Mark the seed file as "Embedded Resource" in Visual Studio properties + +## Running + +```bash +cd eCommerceApi.dsills735/Sills.GolfShop.eCommerce +dotnet clean +dotnet build +dotnet run +``` + +The app will automatically seed the database on first run if empty. + +## API Endpoints + +- `GET /api/category` - Get all categories +- `GET /api/category/{id}` - Get category by ID +- `POST /api/category` - Create category +- `GET /api/product` - Get all products +- `GET /api/product/{id}` - Get product by ID +- `POST /api/product` - Create product +- `GET /api/sales/report` - Generate PDF report +- `POST /api/seed/bulk` - Manually seed database + +## Swagger Documentation + +Access Swagger UI at: `https://localhost:7070/swagger` + +## Technologies + +- .NET 10 +- Entity Framework Core 10 +- SQL Server +- ExcelDataReader - For Excel file parsing +- CsvHelper - For CSV parsing +- QuestPDF - For PDF report generation +- Spectre.Console - For console formatting +- Azure Storage Blobs - For cloud storage diff --git a/eCommerceApi.dsills735/.gitattributes b/eCommerceApi.dsills735/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/eCommerceApi.dsills735/.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.dsills735/.gitignore b/eCommerceApi.dsills735/.gitignore new file mode 100644 index 0000000..9491a2f --- /dev/null +++ b/eCommerceApi.dsills735/.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.dsills735/Sills.GolfShop.eCommerce.slnx b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce.slnx new file mode 100644 index 0000000..b355a29 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce.slnx @@ -0,0 +1,3 @@ + + + diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/CategoryController.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/CategoryController.cs new file mode 100644 index 0000000..04df079 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/CategoryController.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Mvc; +using Sills.GolfShop.eCommerceAPI.Models; +using Sills.GolfShop.eCommerceAPI.Services; +using Sills.GolfShop.eCommerceAPI.Helpers; +using Microsoft.EntityFrameworkCore; + +namespace Sills.GolfShop.eCommerceAPI.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class CategoryController(ICategoryService categoryService) : ControllerBase + +{ + private readonly ICategoryService _categoryService = categoryService; + + [HttpGet] + public async Task>> GetAllCategories([FromQuery] PaginationParameters param, [FromQuery] CategoryParameters categoryParams) + { + var query = _categoryService.GetAllCategoriesQuery(); + query = categoryParams.sortBy switch + { + "name_desc" => query.OrderByDescending(p => p.Name), + _ => query.OrderBy(p => p.Name) + }; + + var pagedCategories = await _categoryService + .GetPagedCategoriesAsync(param.PageNumber, param.PageSize); + + return Ok(pagedCategories); + } + + [HttpGet("{id}")] + public async Task> GetCategoryById(int id) + { + var category = await _categoryService.GetCategoryByIdAsync(id); + if (category == null) + { + return NotFound(); + } + return Ok(category); + } + + [HttpPost] + public async Task> CreateCategory(Categories category) + { + var createdCategory = await _categoryService.CreateCategoryAsync(category); + return CreatedAtAction(nameof(GetCategoryById), new { id = createdCategory.Id }, createdCategory); + } + + [HttpPut("{id}")] + public async Task UpdateCategory(int id, Categories category) + { + var existingCategory = await _categoryService.GetCategoryByIdAsync(id); + if (existingCategory == null) + { + return NotFound(); + } + await _categoryService.UpdateCategoryAsync(id, category); + return NoContent(); + } + [HttpDelete("{id}")] + public async Task DeleteCategory(int id) + { + var existingCategory = await _categoryService.GetCategoryByIdAsync(id); + if (existingCategory == null) + { + return NotFound(); + } + await _categoryService.DeleteCategoryAsync(id); + return NoContent(); + } + +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/ProductController.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/ProductController.cs new file mode 100644 index 0000000..b924f05 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/ProductController.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Mvc; +using Sills.GolfShop.eCommerceAPI.DTO; +using Sills.GolfShop.eCommerceAPI.Helpers; +using Sills.GolfShop.eCommerceAPI.Models; +using Sills.GolfShop.eCommerceAPI.Services; + +namespace Sills.GolfShop.eCommerceAPI.Controllers; + +[ApiController] +[Route("api/[controller]")] + +public class ProductController(IProductsService productsService) : ControllerBase +{ + private readonly IProductsService _productService = productsService; + + + [HttpGet] + public async Task>> GetAllProductsAsync([FromQuery] PaginationParameters param) + { + var pagedProducts = await _productService + .GetPagedProductsAsync(param.PageNumber, param.PageSize); + + return Ok(pagedProducts); + } + + [HttpGet("{id}")] + public async Task> GetProductById(int id) + { + var product = await _productService.GetProductByIdAsync(id); + if (product == null) + { + return NotFound(); + } + return Ok(product); + } + [HttpPost] + public async Task> CreateProduct(Product product) + { + var createdProduct = await _productService.CreateProductAsync(product); + return CreatedAtAction(nameof(GetProductById), new { id = createdProduct.Id }, createdProduct); + } + + [HttpPut("{id}")] + public async Task UpdateProduct(int id, ProductUpdateDto productUpdateDto) + { + + + var existingProduct = await _productService.GetProductByIdAsync(id); + if (existingProduct == null) + { + return NotFound(); + } + existingProduct.Name = productUpdateDto.Name; + existingProduct.Description = productUpdateDto.Description; + existingProduct.QuantityInStock = productUpdateDto.QuantityInStock; + + await _productService.UpdateProductAsync(id, existingProduct); + return NoContent(); + } + + [HttpDelete("{id}")] + public async Task DeleteProduct(int id) + { + var existingProduct = await _productService.GetProductByIdAsync(id); + if (existingProduct == null) + { + return NotFound(); + } + await _productService.DeleteProductAsync(id); + + return NoContent(); + } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/SalesController.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/SalesController.cs new file mode 100644 index 0000000..3c2c4eb --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/SalesController.cs @@ -0,0 +1,61 @@ +using Sills.GolfShop.eCommerceAPI.Services; +using Microsoft.AspNetCore.Mvc; +using Sills.GolfShop.eCommerceAPI.Models; +using Sills.GolfShop.eCommerceAPI.Helpers; + +namespace Sills.GolfShop.eCommerceAPI.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class SalesController(ISalesService salesService) : ControllerBase +{ + private readonly ISalesService _salesService = salesService; + + [HttpGet] + public async Task>> GetAllSales([FromQuery] PaginationParameters param) + { + var pagedSales = await _salesService + .GetPagedSalesAsync(param.PageNumber, param.PageSize); + return Ok(pagedSales); + } + [HttpGet("{id}")] + public async Task> GetSaleById(int id) + { + var sale = await _salesService.GetSaleByIdAsync(id); + if (sale == null) + { + return NotFound(); + } + return Ok(sale); + } + [HttpPost] + public async Task> CreateSale(Sales sale) + { + var createdSale = await _salesService.CreateSaleAsync(sale); + return CreatedAtAction(nameof(GetSaleById), new { id = createdSale.Id }, createdSale); + } + [HttpPut("{id}")] + public async Task UpdateSale(int id, Sales sale) + { + var existingSale = await _salesService.GetSaleByIdAsync(id); + if (existingSale == null) + { + return NotFound(); + } + await _salesService.UpdateSaleAsync(id, sale); + return NoContent(); + } + + [HttpDelete("{id}")] + public async Task DeleteSale(int id) + { + var existingSale = await _salesService.GetSaleByIdAsync(id); + if (existingSale == null) + { + return NotFound(); + } + await _salesService.DeleteSaleAsync(id); + return NoContent(); + } + +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/SeedController.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/SeedController.cs new file mode 100644 index 0000000..68eedee --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Controllers/SeedController.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Mvc; +using Sills.GolfShop.eCommerceAPI.DTO; +using Sills.GolfShop.eCommerceAPI.Models; +using Sills.GolfShop.eCommerceAPI.Services; +using System.Threading.Tasks; + +namespace Sills.GolfShop.eCommerceAPI.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class SeedController(ICategoryService categoryService, IProductsService productService) : ControllerBase +{ + private readonly ICategoryService _categoryService = categoryService; + private readonly IProductsService _productService = productService; + + [HttpPost("bulk")] + public async Task BulkSeed([FromBody] BulkSeedPayloadDto payload) + { + if (payload == null) + { + return BadRequest("Payload cannot be empty."); + } + + foreach (var categoryDto in payload.Categories) + { + var categoryEntity = new Categories + { + Name = categoryDto.Name, + Description = categoryDto.Description + }; + + await _categoryService.CreateCategoryAsync(categoryEntity); + } + + + foreach (var productDto in payload.Products) + { + var productEntity = new Product + { + Name = productDto.Name, + Description = productDto.Description, + QuantityInStock = productDto.QuantityInStock + }; + await _productService.CreateProductAsync(productEntity); + } + + return Ok(new + { + Message = "Bulk seeding completed successfully.", + CategoriesSeeded = payload.Categories.Count, + ProductsSeeded = payload.Products.Count + }); + } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/DTO/BulkSeedingDto.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/DTO/BulkSeedingDto.cs new file mode 100644 index 0000000..93dcbf4 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/DTO/BulkSeedingDto.cs @@ -0,0 +1,7 @@ +namespace Sills.GolfShop.eCommerceAPI.DTO +{ + public sealed record BulkSeedPayloadDto( + List Categories, + List Products + ); +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/DTO/ProductUpdateDTO.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/DTO/ProductUpdateDTO.cs new file mode 100644 index 0000000..667c7d7 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/DTO/ProductUpdateDTO.cs @@ -0,0 +1,3 @@ +namespace Sills.GolfShop.eCommerceAPI.DTO; + +public sealed record ProductUpdateDto(string Name, string Description, int QuantityInStock); diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/DTO/SeedingDto.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/DTO/SeedingDto.cs new file mode 100644 index 0000000..4cb2484 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/DTO/SeedingDto.cs @@ -0,0 +1,5 @@ +namespace Sills.GolfShop.eCommerceAPI.DTO; + +public sealed record SeedingProductDto(string Name, string Description, int QuantityInStock); + +public sealed record SeedingCategoryDto(string Name, string Description); diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Data/GolfShopDbContext.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Data/GolfShopDbContext.cs new file mode 100644 index 0000000..cf0b345 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Data/GolfShopDbContext.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using Sills.GolfShop.eCommerceAPI.Models; + +namespace Sills.GolfShop.eCommerceAPI.Data; + +public class GolfShopDbContext(DbContextOptions options) : DbContext(options) +{ + + public DbSet Products { get; set; } + public DbSet Categories { get; set; } + public DbSet Sales { get; set; } + public DbSet ProductSales { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasKey(ps => new { ps.ProductID, ps.SaleID }); + + modelBuilder.Entity() + .Property(p => p.Price) + .HasColumnType("decimal(18,2)"); + } +} + + diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Document_Processing/Reader.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Document_Processing/Reader.cs new file mode 100644 index 0000000..345c1ea --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Document_Processing/Reader.cs @@ -0,0 +1,223 @@ +using CsvHelper; +using ExcelDataReader; +using Sills.GolfShop.eCommerceAPI.DTO; +using System.Data; +using System.Globalization; +using System.Reflection; +using System.Text; + +namespace Sills.GolfShop.eCommerceAPI.Document_Processing; + +public class Reader +{ + public async Task ReadAndSeedAsync(string apiEndpointUrl, string resourceName) + { + try + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + var assembly = Assembly.GetExecutingAssembly(); + var categories = new List(); + var products = new List(); + + using (var stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream == null) + { + throw new FileNotFoundException($"Embedded resource '{resourceName}' not found. Ensure the file is added to the project and marked as an embedded resource."); + } + + if (resourceName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase)) + { + (categories, products) = await ReadCsvAsync(stream); + } + else if (resourceName.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase) || + resourceName.EndsWith(".xls", StringComparison.OrdinalIgnoreCase)) + { + (categories, products) = await ReadExcelAsync(stream); + } + else + { + throw new InvalidOperationException($"Unsupported file format: {Path.GetExtension(resourceName)}. Supported formats: .xlsx, .xls, .csv"); + } + } + + var payload = new BulkSeedPayloadDto(categories, products); + return await Task.FromResult(payload); + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] Failed to read spreadsheet: {ex.Message}"); + throw; + } + } + + private async Task<(List, List)> ReadExcelAsync(Stream stream) + { + var categories = new List(); + var products = new List(); + + try + { + using (var reader = ExcelReaderFactory.CreateReader(stream)) + { + var config = new ExcelDataSetConfiguration + { + ConfigureDataTable = _ => new ExcelDataTableConfiguration + { + UseHeaderRow = true + } + }; + var dataset = reader.AsDataSet(config); + + if (dataset.Tables.Contains("Categories")) + { + DataTable categoryTable = dataset.Tables["Categories"]; + foreach (DataRow row in categoryTable.Rows) + { + try + { + categories.Add(new SeedingCategoryDto( + Name: row["Name"]?.ToString()?.Trim() ?? string.Empty, + Description: row["Description"]?.ToString()?.Trim() ?? string.Empty + )); + } + catch (Exception ex) + { + Console.WriteLine($"[WARNING] Skipped invalid category row: {ex.Message}"); + } + } + } + + if (dataset.Tables.Contains("Products")) + { + DataTable productTable = dataset.Tables["Products"]; + foreach (DataRow row in productTable.Rows) + { + try + { + int quantity = 0; + if (row["QuantityInStock"] != DBNull.Value && + int.TryParse(row["QuantityInStock"].ToString(), out int qty)) + { + quantity = qty; + } + + products.Add(new SeedingProductDto( + Name: row["Name"]?.ToString()?.Trim() ?? string.Empty, + Description: row["Description"]?.ToString()?.Trim() ?? string.Empty, + QuantityInStock: quantity + )); + } + catch (Exception ex) + { + Console.WriteLine($"[WARNING] Skipped invalid product row: {ex.Message}"); + } + } + } + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to parse Excel file: {ex.Message}", ex); + } + + return (categories, products); + } + + private async Task<(List, List)> ReadCsvAsync(Stream stream) + { + var categories = new List(); + var products = new List(); + + try + { + using (var reader = new StreamReader(stream, Encoding.UTF8)) + using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) + { + var records = csv.GetRecords().ToList(); + + int separatorIndex = -1; + for (int i = 0; i < records.Count; i++) + { + var record = (IDictionary)records[i]; + if (record.Values.All(v => v == null || string.IsNullOrWhiteSpace(v.ToString()))) + { + separatorIndex = i; + break; + } + } + + if (separatorIndex == -1) + { + separatorIndex = records.Count / 2; + } + + for (int i = 0; i < separatorIndex; i++) + { + try + { + var record = (IDictionary)records[i]; + if (record.TryGetValue("Name", out var nameObj) && nameObj != null) + { + categories.Add(new SeedingCategoryDto( + Name: nameObj.ToString()?.Trim() ?? string.Empty, + Description: record.TryGetValue("Description", out var descObj) + ? descObj?.ToString()?.Trim() ?? string.Empty + : string.Empty + )); + } + } + catch (Exception ex) + { + Console.WriteLine($"[WARNING] Skipped invalid category row: {ex.Message}"); + } + } + + for (int i = separatorIndex + 1; i < records.Count; i++) + { + try + { + var record = (IDictionary)records[i]; + if (record.TryGetValue("Name", out var nameObj) && nameObj != null) + { + int quantity = 0; + if (record.TryGetValue("QuantityInStock", out var qtyObj) && qtyObj != null) + { + int.TryParse(qtyObj.ToString(), out quantity); + } + + products.Add(new SeedingProductDto( + Name: nameObj.ToString()?.Trim() ?? string.Empty, + Description: record.TryGetValue("Description", out var descObj) + ? descObj?.ToString()?.Trim() ?? string.Empty + : string.Empty, + QuantityInStock: quantity + )); + } + } + catch (Exception ex) + { + Console.WriteLine($"[WARNING] Skipped invalid product row: {ex.Message}"); + } + } + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to parse CSV file: {ex.Message}", ex); + } + + return (categories, products); + } + + public static List GetAvailableResources() + { + var assembly = Assembly.GetExecutingAssembly(); + return assembly.GetManifestResourceNames() + .Where(name => name.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase) || + name.EndsWith(".xls", StringComparison.OrdinalIgnoreCase) || + name.EndsWith(".csv", StringComparison.OrdinalIgnoreCase)) + .ToList(); + } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Document_Processing/ReportGenerator.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Document_Processing/ReportGenerator.cs new file mode 100644 index 0000000..5a025e5 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Document_Processing/ReportGenerator.cs @@ -0,0 +1,124 @@ +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Microsoft.EntityFrameworkCore; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using Sills.GolfShop.eCommerceAPI.Models; +using Sills.GolfShop.eCommerceAPI.Services; + +namespace Sills.GolfShop.eCommerceAPI.Document_Processing; + +public class ReportGenerator +{ + public static async Task CreateAndUploadInventoryReportAsync( + ICategoryService categoryService, + IProductsService productsService, + BlobServiceClient blobServiceClient) + { + QuestPDF.Settings.License = LicenseType.Community; + + + var categories = await categoryService.GetAllCategoriesQuery().ToListAsync(); + + var products = await productsService.GetAllProductsAsync(); + + using var pdfStream = new MemoryStream(); + + Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + page.DefaultTextStyle(x => x.FontSize(12).FontColor(Colors.Black)); + + page.Header().Column(column => + { + column.Item().Text("Inventory Report").SemiBold().FontSize(20).FontColor(Colors.Blue.Medium); + column.Item().Text($"Sills Golf Shop Inventory Report - {DateTime.Now:MMMM dd, yyyy}").Bold().FontSize(14).FontColor(Colors.Blue.Medium); + }); + + page.Content().PaddingVertical(1, Unit.Centimetre).Column(column => + { + + column.Item().Text("Categories").Bold().FontSize(16).Underline(); + column.Item().PaddingBottom(0.5f, Unit.Centimetre).Table(table => + { + table.ColumnsDefinition(columns => + { + columns.ConstantColumn(50); + columns.RelativeColumn(); + }); + + table.Header(header => + { + header.Cell().Element(CellStyle).Text("ID").SemiBold(); + header.Cell().Element(CellStyle).Text("Name").SemiBold(); + }); + + foreach (var category in categories) + { + table.Cell().Element(CellStyle).Text(category.Id.ToString()); + table.Cell().Element(CellStyle).Text(category.Name); + } + }); + + + column.Item().Text("Products").Bold().FontSize(16).Underline(); + column.Item().Table(table => + { + table.ColumnsDefinition(columns => + { + columns.ConstantColumn(40); + columns.RelativeColumn(2); + columns.RelativeColumn(3); + columns.RelativeColumn(1); + columns.RelativeColumn(1); + columns.RelativeColumn(1); + }); + + table.Header(header => + { + header.Cell().Element(CellStyle).Text("ID").SemiBold(); + header.Cell().Element(CellStyle).Text("Name").SemiBold(); + header.Cell().Element(CellStyle).Text("Description").SemiBold(); + header.Cell().Element(CellStyle).Text("Price").SemiBold(); + header.Cell().Element(CellStyle).Text("Qty").SemiBold(); + header.Cell().Element(CellStyle).Text("Cat ID").SemiBold(); + }); + + foreach (var product in products) + { + table.Cell().Element(CellStyle).Text(product.Id.ToString()); + table.Cell().Element(CellStyle).Text(product.Name); + table.Cell().Element(CellStyle).Text(product.Description); + table.Cell().Element(CellStyle).Text($"${product.Price:F2}"); + table.Cell().Element(CellStyle).Text(product.QuantityInStock.ToString()); + table.Cell().Element(CellStyle).Text(product.CategoryId.ToString()); + } + }); + }); + }); + }) + .GeneratePdf(pdfStream); + + pdfStream.Position = 0; + + var containerClient = blobServiceClient.GetBlobContainerClient("reports"); + await containerClient.CreateIfNotExistsAsync(PublicAccessType.None); + + string blobName = $"inventory-reports/InventoryReport_{DateTime.UtcNow:yyyyMMddHHmmss}.pdf"; + var blobClient = containerClient.GetBlobClient(blobName); + + await blobClient.UploadAsync(pdfStream, new BlobHttpHeaders { ContentType = "application/pdf" }); + + return blobClient.Uri.ToString(); + } + + private static IContainer CellStyle(IContainer container) + { + return container.Border(1).BorderColor(Colors.Grey.Lighten2).Padding(5); + } +} + diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Document_Processing/ResourceDebugger.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Document_Processing/ResourceDebugger.cs new file mode 100644 index 0000000..f6f6baf --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Document_Processing/ResourceDebugger.cs @@ -0,0 +1,56 @@ +using System.Reflection; + +namespace Sills.GolfShop.eCommerceAPI.Document_Processing; + +public static class ResourceDebugger +{ + public static void PrintAllResources() + { + var assembly = Assembly.GetExecutingAssembly(); + var resources = assembly.GetManifestResourceNames(); + + Console.WriteLine("\n[DEBUG] Available Embedded Resources:"); + Console.WriteLine("====================================="); + + if (resources.Length == 0) + { + Console.WriteLine(" (No resources found)"); + } + else + { + foreach (var resource in resources.OrderBy(r => r)) + { + Console.WriteLine($" • {resource}"); + } + } + + Console.WriteLine("=====================================\n"); + } + + public static void PrintSpreadsheetResources() + { + var assembly = Assembly.GetExecutingAssembly(); + var spreadsheetResources = assembly.GetManifestResourceNames() + .Where(name => name.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase) || + name.EndsWith(".xls", StringComparison.OrdinalIgnoreCase) || + name.EndsWith(".csv", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + Console.WriteLine("\n[DEBUG] Available Spreadsheet Resources:"); + Console.WriteLine("====================================="); + + if (spreadsheetResources.Count == 0) + { + Console.WriteLine(" (No spreadsheet resources found!)"); + } + else + { + foreach (var resource in spreadsheetResources) + { + Console.WriteLine($" • {resource}"); + } + } + + Console.WriteLine("=====================================\n"); + } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Helpers/PaginationParameters.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Helpers/PaginationParameters.cs new file mode 100644 index 0000000..9361162 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Helpers/PaginationParameters.cs @@ -0,0 +1,14 @@ +namespace Sills.GolfShop.eCommerceAPI.Helpers; + +public class PaginationParameters +{ + public const int MaxPageSize = 50; + public int PageNumber { get; set; } = 1; + + private int _pageSize = 10; + public int PageSize + { + get => _pageSize; + set => _pageSize = (value > MaxPageSize) ? MaxPageSize : value; + } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Helpers/SortingParameters.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Helpers/SortingParameters.cs new file mode 100644 index 0000000..2973493 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Helpers/SortingParameters.cs @@ -0,0 +1,20 @@ +namespace Sills.GolfShop.eCommerceAPI.Helpers; + +public class ProductParameters +{ + public string? name { get; set; } + public decimal? minPrice { get; set; } + public decimal? maxPrice { get; set; } + + public string? sortBy { get; set; } = null; +} + +public class CategoryParameters +{ + public string? name { get; set; } + public string? sortBy { get; set; } = null; +} + public class SalesParameters +{ + public string? sortBy { get; set; } = null; +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Mapping/ProductMapper.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Mapping/ProductMapper.cs new file mode 100644 index 0000000..280037c --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Mapping/ProductMapper.cs @@ -0,0 +1,15 @@ +namespace Sills.GolfShop.eCommerceAPI.Mapping; + +public static class ProductMapper +{ + public static DTO.ProductUpdateDto? ToDTO(this Models.Product product) + { + if (product == null) return null; + + return new DTO.ProductUpdateDto( + product.Name, + product.Description, + product.QuantityInStock + ); + } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Migrations/20260403232616_initial.Designer.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Migrations/20260403232616_initial.Designer.cs new file mode 100644 index 0000000..da26d3e --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Migrations/20260403232616_initial.Designer.cs @@ -0,0 +1,152 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Sills.GolfShop.eCommerceAPI.Data; + +#nullable disable + +namespace Sills.GolfShop.eCommerceAPI.Migrations +{ + [DbContext(typeof(GolfShopDbContext))] + [Migration("20260403232616_initial")] + partial class initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.Categories", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("QuantityInStock") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.ProductSales", b => + { + b.Property("ProductID") + .HasColumnType("int"); + + b.Property("SaleID") + .HasColumnType("int"); + + b.HasKey("ProductID", "SaleID"); + + b.HasIndex("SaleID"); + + b.ToTable("ProductSales"); + }); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.Sales", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("customerName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("shippingAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Sales"); + }); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.ProductSales", b => + { + b.HasOne("Sills.GolfShop.eCommerceAPI.Models.Product", "Product") + .WithMany("ProductSales") + .HasForeignKey("ProductID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Sills.GolfShop.eCommerceAPI.Models.Sales", "Sale") + .WithMany("ProductSales") + .HasForeignKey("SaleID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Sale"); + }); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.Product", b => + { + b.Navigation("ProductSales"); + }); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.Sales", b => + { + b.Navigation("ProductSales"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Migrations/20260403232616_initial.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Migrations/20260403232616_initial.cs new file mode 100644 index 0000000..26705a9 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Migrations/20260403232616_initial.cs @@ -0,0 +1,107 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Sills.GolfShop.eCommerceAPI.Migrations +{ + /// + public partial class initial : 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), + Description = table.Column(type: "nvarchar(max)", nullable: false), + DeletedAt = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", 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), + Description = table.Column(type: "nvarchar(max)", nullable: false), + Price = table.Column(type: "decimal(18,2)", nullable: false), + QuantityInStock = table.Column(type: "int", nullable: false), + DeletedAt = table.Column(type: "datetime2", nullable: true), + CategoryId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Sales", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + customerName = table.Column(type: "nvarchar(max)", nullable: false), + shippingAddress = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Sales", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ProductSales", + columns: table => new + { + ProductID = table.Column(type: "int", nullable: false), + SaleID = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductSales", x => new { x.ProductID, x.SaleID }); + table.ForeignKey( + name: "FK_ProductSales_Products_ProductID", + column: x => x.ProductID, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ProductSales_Sales_SaleID", + column: x => x.SaleID, + principalTable: "Sales", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ProductSales_SaleID", + table: "ProductSales", + column: "SaleID"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Categories"); + + migrationBuilder.DropTable( + name: "ProductSales"); + + migrationBuilder.DropTable( + name: "Products"); + + migrationBuilder.DropTable( + name: "Sales"); + } + } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Migrations/GolfShopDbContextModelSnapshot.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Migrations/GolfShopDbContextModelSnapshot.cs new file mode 100644 index 0000000..7bbf976 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Migrations/GolfShopDbContextModelSnapshot.cs @@ -0,0 +1,149 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Sills.GolfShop.eCommerceAPI.Data; + +#nullable disable + +namespace Sills.GolfShop.eCommerceAPI.Migrations +{ + [DbContext(typeof(GolfShopDbContext))] + partial class GolfShopDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.Categories", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("QuantityInStock") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.ProductSales", b => + { + b.Property("ProductID") + .HasColumnType("int"); + + b.Property("SaleID") + .HasColumnType("int"); + + b.HasKey("ProductID", "SaleID"); + + b.HasIndex("SaleID"); + + b.ToTable("ProductSales"); + }); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.Sales", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("customerName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("shippingAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Sales"); + }); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.ProductSales", b => + { + b.HasOne("Sills.GolfShop.eCommerceAPI.Models.Product", "Product") + .WithMany("ProductSales") + .HasForeignKey("ProductID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Sills.GolfShop.eCommerceAPI.Models.Sales", "Sale") + .WithMany("ProductSales") + .HasForeignKey("SaleID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Sale"); + }); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.Product", b => + { + b.Navigation("ProductSales"); + }); + + modelBuilder.Entity("Sills.GolfShop.eCommerceAPI.Models.Sales", b => + { + b.Navigation("ProductSales"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/Categories.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/Categories.cs new file mode 100644 index 0000000..d2073c4 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/Categories.cs @@ -0,0 +1,11 @@ +namespace Sills.GolfShop.eCommerceAPI.Models +{ + public class Categories + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public DateTime? DeletedAt { get; set; } + + } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/Product.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/Product.cs new file mode 100644 index 0000000..ac22a65 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/Product.cs @@ -0,0 +1,14 @@ +namespace Sills.GolfShop.eCommerceAPI.Models; + +public class Product +{ + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; set; } + public int QuantityInStock { get; set; } + public DateTime? DeletedAt { get; set; } + + public List ProductSales { get; } = []; + public int CategoryId { get; internal set; } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/ProductSales.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/ProductSales.cs new file mode 100644 index 0000000..09f7e23 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/ProductSales.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; + +namespace Sills.GolfShop.eCommerceAPI.Models; + +public class ProductSales +{ + public int ProductID { get; set; } + public int SaleID { get; set; } + public Product Product { get; set; } = null!; + public Sales Sale { get; set; } = null!; + + +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/Sales.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/Sales.cs new file mode 100644 index 0000000..afccd6e --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Models/Sales.cs @@ -0,0 +1,11 @@ +namespace Sills.GolfShop.eCommerceAPI.Models; + +public class Sales +{ + public int Id { get; set; } + public string customerName { get; set; } + + public string shippingAddress { get; set; } = string.Empty; + + public List ProductSales { get; } = []; +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Postman_Collection/PostmanCollection.json b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Postman_Collection/PostmanCollection.json new file mode 100644 index 0000000..a92647c --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Postman_Collection/PostmanCollection.json @@ -0,0 +1,241 @@ +{ + "info": { + "name": "Sills Golf Shop eCommerce API (Updated)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Products", + "item": [ + { + "name": "Get All Products (Paged)", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/api/Product?pageNumber=1&pageSize=10", + "host": [ "{{baseUrl}}" ], + "path": [ "api", "Product" ], + "query": [ + { + "key": "pageNumber", + "value": "1" + }, + { + "key": "pageSize", + "value": "10" + } + ] + } + } + }, + { + "name": "Get Product by Id", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/Product/1" + } + }, + { + "name": "Create Product", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Driver\",\n \"description\": \"Titanium face driver\",\n \"price\": 499.99,\n \"quantityInStock\": 25,\n \"categoryId\": 1\n}" + }, + "url": "{{baseUrl}}/api/Product" + } + }, + { + "name": "Update Product", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Updated Driver\",\n \"description\": \"Updated description\",\n \"quantityInStock\": 20\n}" + }, + "url": "{{baseUrl}}/api/Product/1" + } + }, + { + "name": "Delete Product", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/Product/1" + } + } + ] + }, + { + "name": "Categories", + "item": [ + { + "name": "Get All Categories (Paged + Sort)", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/api/Category?pageNumber=1&pageSize=10&sortBy=name", + "host": [ "{{baseUrl}}" ], + "path": [ "api", "Category" ], + "query": [ + { + "key": "pageNumber", + "value": "1" + }, + { + "key": "pageSize", + "value": "10" + }, + { + "key": "sortBy", + "value": "name" + } + ] + } + } + }, + { + "name": "Get Category by Id", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/Category/1" + } + }, + { + "name": "Create Category", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Clubs\",\n \"description\": \"Drivers, Irons, Putters\"\n}" + }, + "url": "{{baseUrl}}/api/Category" + } + }, + { + "name": "Update Category", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Updated Category\",\n \"description\": \"Updated description\"\n}" + }, + "url": "{{baseUrl}}/api/Category/1" + } + }, + { + "name": "Delete Category", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/Category/1" + } + } + ] + }, + { + "name": "Sales", + "item": [ + { + "name": "Get All Sales (Paged)", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/api/Sales?pageNumber=1&pageSize=10", + "host": [ "{{baseUrl}}" ], + "path": [ "api", "Sales" ], + "query": [ + { + "key": "pageNumber", + "value": "1" + }, + { + "key": "pageSize", + "value": "10" + } + ] + } + } + }, + { + "name": "Get Sale by Id", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/Sales/1" + } + }, + { + "name": "Create Sale", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"customerName\": \"John Doe\",\n \"shippingAddress\": \"123 Main St\"\n}" + }, + "url": "{{baseUrl}}/api/Sales" + } + }, + { + "name": "Update Sale", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"customerName\": \"Updated Name\",\n \"shippingAddress\": \"Updated Address\"\n}" + }, + "url": "{{baseUrl}}/api/Sales/1" + } + }, + { + "name": "Delete Sale", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/Sales/1" + } + } + ] + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "https://localhost:8080", + "type": "string" + } + ] +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Program.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Program.cs new file mode 100644 index 0000000..6a132db --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Program.cs @@ -0,0 +1,173 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.OpenApi; +using Sills.GolfShop.eCommerceAPI.Data; +using Sills.GolfShop.eCommerceAPI.Services; +using Sills.GolfShop.eCommerceAPI.Document_Processing; +using Sills.GolfShop.eCommerceAPI.Models; +using Spectre.Console; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddControllers(); +builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + using (var scope = app.Services.CreateScope()) + { + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.EnsureDeleted(); + dbContext.Database.EnsureCreated(); + + await AutoSeedDatabaseAsync(dbContext, scope); + } +} + +app.MapControllers(); + +app.Run(); + +static async Task AutoSeedDatabaseAsync(GolfShopDbContext dbContext, IServiceScope scope) +{ + try + { + bool hasCategories = await dbContext.Categories.AnyAsync(); + bool hasProducts = await dbContext.Products.AnyAsync(); + + if (hasCategories && hasProducts) + { + AnsiConsole.MarkupLine("[green]✓ Database already populated with data.[/]"); + return; + } + + AnsiConsole.MarkupLine("[yellow]→ Database is empty. Attempting to seed from spreadsheet...[/]"); + + ResourceDebugger.PrintSpreadsheetResources(); + + var availableResources = Reader.GetAvailableResources(); + if (availableResources.Count == 0) + { + AnsiConsole.MarkupLine("[yellow]⚠ No spreadsheet files found for seeding. Add .xlsx, .xls, or .csv files to Resources folder as embedded resources.[/]"); + return; + } + + string seedFile = availableResources.First(); + AnsiConsole.MarkupLine($"[cyan]→ Found seed file: {Path.GetFileName(seedFile)}[/]"); + + var reader = scope.ServiceProvider.GetRequiredService(); + var categoryService = scope.ServiceProvider.GetRequiredService(); + var productService = scope.ServiceProvider.GetRequiredService(); + + try + { + var payload = await reader.ReadAndSeedAsync("", seedFile); + + if (payload.Categories.Count == 0 && payload.Products.Count == 0) + { + AnsiConsole.MarkupLine("[yellow]⚠ Spreadsheet is empty or has no valid data.[/]"); + return; + } + + AnsiConsole.MarkupLine($"[cyan]→ Seeding {payload.Categories.Count} categories and {payload.Products.Count} products...[/]"); + + foreach (var categoryDto in payload.Categories) + { + var categoryEntity = new Sills.GolfShop.eCommerceAPI.Models.Categories + { + Name = categoryDto.Name, + Description = categoryDto.Description + }; + await categoryService.CreateCategoryAsync(categoryEntity); + } + + foreach (var productDto in payload.Products) + { + var productEntity = new Sills.GolfShop.eCommerceAPI.Models.Product + { + Name = productDto.Name, + Description = productDto.Description, + QuantityInStock = productDto.QuantityInStock + }; + await productService.CreateProductAsync(productEntity); + } + + await DisplaySeededDataAsync(dbContext); + + AnsiConsole.MarkupLine("[green]✓ Database seeding completed successfully![/]"); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]✗ Seeding failed: {ex.Message}[/]"); + } + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]✗ Auto-seeding error: {ex.Message}[/]"); + } +} + +static async Task DisplaySeededDataAsync(GolfShopDbContext dbContext) +{ + try + { + var categories = await dbContext.Categories.ToListAsync(); + var products = await dbContext.Products.ToListAsync(); + + if (categories.Count > 0) + { + AnsiConsole.MarkupLine("\n[bold cyan]📋 Categories[/]"); + var categoryTable = new Table() + .AddColumn("ID") + .AddColumn("Name") + .AddColumn("Description"); + + foreach (var category in categories) + { + categoryTable.AddRow( + category.Id.ToString(), + category.Name ?? "-", + category.Description ?? "-" + ); + } + AnsiConsole.Write(categoryTable); + } + + if (products.Count > 0) + { + AnsiConsole.MarkupLine("\n[bold cyan]📦 Products[/]"); + var productTable = new Table() + .AddColumn("ID") + .AddColumn("Name") + .AddColumn("Description") + .AddColumn("Qty") + .AddColumn("Category ID"); + + foreach (var product in products) + { + productTable.AddRow( + product.Id.ToString(), + product.Name ?? "-", + product.Description ?? "-", + product.QuantityInStock.ToString(), + product.CategoryId.ToString() + ); + } + AnsiConsole.Write(productTable); + } + + AnsiConsole.MarkupLine($"\n[green]✓ Loaded {categories.Count} categories and {products.Count} products[/]\n"); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[yellow]⚠ Could not display data: {ex.Message}[/]"); + } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Properties/launchSettings.json b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Properties/launchSettings.json new file mode 100644 index 0000000..8cb9f9e --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/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:8080", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7070;http://localhost:8080", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Resources/DataToSeed.csv b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Resources/DataToSeed.csv new file mode 100644 index 0000000..e53bcfb --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Resources/DataToSeed.csv @@ -0,0 +1,141 @@ +Name,Description +Drivers,Premium equipment used to hit the ball from the tee box with maximum distance +Fairway Woods,Versatile clubs designed for accuracy and distance from the fairway +Hybrids,Modern clubs that combine characteristics of irons and woods for improved playability +Irons,Essential clubs for mid-range shots with precision and control +Wedges,Short-distance clubs for approach shots around the green +Putters,Specialized clubs used on the green to complete each hole +Golf Balls,High-performance balls designed for distance and control +Golf Bags,Durable bags to carry and organize your golf equipment during play +Golf Shoes,Professional footwear designed for comfort and traction on the course +Golf Gloves,Protective hand wear for improved grip and comfort +Golf Apparel - Shirts,Comfortable and breathable tops for playing in various conditions +Golf Apparel - Pants,Professional trousers designed for mobility and style on the course +Golf Apparel - Jackets,Weather-resistant outerwear to keep you comfortable in any season +Golf Accessories,Small items like tees, markers, and divot tools for everyday play +Training Aids,Equipment designed to improve your swing and golf technique +Golf Rangefinders,Devices to measure distance to fairway and green targets +Golf GPS Watches,Wearable technology for on-course navigation and statistics +Golf Towels,Quality towels for cleaning clubs and drying hands during play +Golf Umbrellas,Weather protection for rainy days on the course +Golf Push Carts,Lightweight carts to transport your bag without carrying + +Callaway Paradym Driver,High-performance driver with advanced aerodynamics for maximum distance,45 +TaylorMade Stealth Plus Driver,Precision-forged driver with optimal weight distribution,38 +Titleist TSR2 Driver,Player-friendly driver with excellent forgiveness,42 +Ping G425 Driver,Forgiving driver with adjustable loft settings,35 +Cobra Radspeed XB Driver,Extreme forgiveness driver for slower swing speeds,52 + +Callaway Paradym Fairway Wood,Versatile fairway wood for accurate long shots,28 +TaylorMade Stealth Plus Fairway Wood,Distance-focused fairway wood with premium feel,31 +Titleist TSR2 Fairway Wood,Game-improvement fairway wood with solid performance,25 +Ping G425 Fairway Wood,Forgiving fairway wood for consistent results,29 +Cobra Radspeed XB Fairway Wood,Maximum forgiveness fairway wood,33 + +Callaway Paradym Hybrid,Playable hybrid for versatile shot-making,18 +TaylorMade Stealth Plus Hybrid,Sophisticated hybrid with excellent ball striking,22 +Titleist TSR Hybrid,Precision hybrid for lower-trajectory shots,20 +Ping G425 Hybrid,Confidence-inspiring hybrid performance,24 +Cobra Radspeed Hybrid,Game-improvement hybrid for all golfers,26 + +Callaway Apex Pro Irons,Tour-quality irons for skilled players,8 +TaylorMade P790 Irons,Forgiving players irons with premium feel,12 +Titleist AP3 Irons,Game-improvement irons for better all-around play,10 +Ping i525 Irons,Distance-oriented irons for moderate swing speeds,15 +Cobra King Forged Irons,Maximum workability with excellent ball control,9 + +Callaway Apex Wedge,Precise wedge for short game mastery,16 +TaylorMade Milled Grind 2 Wedge,Tour-inspired wedge with spin and control,19 +Titleist SM8 Wedge,Premium wedge with exceptional versatility,14 +Ping Glide 4.0 Wedge,Everyday wedge for consistent performance,17 +Cobra King Wedge,Forgiving wedge with added forgiveness,13 + +Callaway Odyssey Stroke Lab Putter,Tour-quality putter with modern technology,5 +TaylorMade Spider X Putter,High-MOI putter for consistent distance control,7 +Titleist Scotty Cameron Phantom X Putter,Premium handcrafted putter for perfectionists,3 +Ping PP59 Putter,Classic putter design with modern performance,6 +Odyssey DFX Putter,Beginner-friendly putter with forgiving design,8 + +Titleist Pro V1 Golf Balls,Premium golf balls for exceptional control and distance,250 +TaylorMade TP5 Golf Balls,Tour-quality balls with premium spin control,220 +Callaway Chrome Soft Golf Balls,High-performance balls with consistent flight,280 +Bridgestone Tour B XS Balls,Tour professional golf balls with exceptional feel,195 +Srixon Z-Star XV Balls,Premium performance balls with yellow visibility,240 + +Callaway Org 14 Golf Bag,14-slot carry bag with premium construction,12 +TaylorMade FlexTech Golf Bag,Lightweight and durable stand bag,15 +Titleist Cart 14 Golf Bag,Full-size cart bag with ample storage,8 +Ping DLX Golf Bag,Premium leather cart bag with elegant design,10 +Odyssey Ai-One Golf Bag,Modern stand bag with innovative features,9 + +FootJoy Pro SL Golf Shoes,Lightweight professional golf shoes with comfort,22 +Nike Infinity Tour Golf Shoes,Waterproof touring shoes with excellent support,18 +Adidas Tour 360 Boost Golf Shoes,Premium golf shoes with stability technology,25 +Puma Grip Fusion Golf Shoes,Innovative grip technology golf shoes,20 +Callaway Coronado Golf Shoes,Casual-professional golf shoes for everyday play,28 + +Callaway Weather Spann Golf Glove,Premium glove with excellent durability,35 +TaylorMade Tour Preferred Golf Glove,Professional tour glove for ultimate feel,32 +Nike Golf Glove,Breathable and comfortable everyday glove,38 +Footjoy WeatherSof Glove,Waterproof glove for all-weather play,40 +Ping Sensor Excel Glove,Cabretta leather glove with outstanding grip,36 + +Nike Dri-FIT Polo Golf Shirt,Breathable performance golf shirt,55 +Callaway Swing Tech Polo,Technology-enhanced golf polo,48 +TaylorMade Performance Polo,Moisture-wicking polo for comfort,52 +Adidas Golf Polo,Premium golf polo with modern styling,45 +Titleist Tour Performance Shirt,Professional tour-quality polo shirt,50 + +Callaway Chev Golf Pants,Comfortable and stylish golf trousers,42 +Nike Flex Golf Pants,Performance pants with mobility,38 +TaylorMade Tour Slim Pants,Professional fit golf pants,40 +Adidas Ultimate Golf Pants,Modern and comfortable golf trousers,44 +Titleist Tour Performance Pants,Premium quality golf pants,39 + +Nike Storm-Fit Golf Jacket,Waterproof and breathable jacket,28 +TaylorMade Tour Response Jacket,Professional tournament-grade jacket,26 +Callaway Aquapel Golf Jacket,Water-resistant golf jacket,30 +Puma Golf Rain Jacket,Lightweight packable rain jacket,32 +Adidas Ultimate Golf Jacket,All-weather protection jacket,29 + +Callaway Golf Tee Pack,Premium wooden tees 100-pack,85 +TaylorMade Restart Golf Tees,High-performance synthetic tees,72 +Golf Ball Markers,Custom ball markers 12-pack,180 +Divot Tool with Marker,Essential course maintenance tool,95 +Golf Towel Clip,Convenient towel attachment,120 + +SuperSpeed Golf Training System,Science-based swing speed training,4 +Orange Whip Golf Trainer,Pre-swing warm-up training tool,6 +SKLZ Gold Flex Golf Trainer,Flexibility and strength training,8 +Impact Snap Training Aid,Wrist lag and power development,7 +Tour Striker Smart Ball,Alignment and swing path training,9 + +Bushnell Pro X3 Rangefinder,Laser rangefinder with slope technology,11 +Nikon Coolshot Pro Stabilized,Vibration reduction rangefinder,8 +Titleist Pro Pinseeker Tour,Tour-level rangefinder accuracy,9 +Garmin Approach Z82,Premium GPS rangefinder watch,5 +Leupold GolfLink Digital,Dependable golf rangefinder,10 + +Garmin Approach S42,Basic GPS golf watch,7 +Garmin Approach S62,Advanced GPS golf watch,5 +Apple Watch Ultra,Versatile smartwatch with golf apps,3 +Callaway Golf Watch,Simple golf-specific GPS watch,8 +Bushnell iON Edge Golf GPS,Advanced wearable GPS device,4 + +Callaway Tour Towel,Premium microfiber golf towel,65 +TaylorMade Performance Towel,High-quality golf towel,58 +Titleist Players Collection Towel,Professional golf towel,62 +Ping Golf Towel,Durable and absorbent towel,70 +Odyssey Golf Towel,Soft and quick-drying towel,66 + +Callaway Umbrella,Large golf umbrella with UV protection,28 +Nike Golf Umbrella,Professional golf umbrella,25 +TaylorMade Tour Umbrella,Tour-quality weather protection,22 +Titleist Golf Umbrella,Premium umbrella for all conditions,20 +Proactive Golf Umbrella,Extra-large umbrella protection,30 + +Callaway Push Cart,3-wheel golf push cart,18 +Sun Mountain Push Cart,Lightweight aluminum push cart,14 +Bag Boy Push Cart,Sturdy push cart with braking system,16 +PGA Tour Pro Push Cart,Premium quality push cart,9 +Izzo Golf Push Cart,Durable and reliable push cart,13 diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Services/ICategoryService.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Services/ICategoryService.cs new file mode 100644 index 0000000..5d248bd --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Services/ICategoryService.cs @@ -0,0 +1,80 @@ +using Microsoft.EntityFrameworkCore; +using Sills.GolfShop.eCommerceAPI.Data; +using Sills.GolfShop.eCommerceAPI.Models; +using Sills.GolfShop.eCommerceAPI.Services; + +namespace Sills.GolfShop.eCommerceAPI.Services; + + + public interface ICategoryService + { + IQueryable GetAllCategoriesQuery(); + Task GetCategoryByIdAsync(int id); + Task CreateCategoryAsync(Categories category); + Task UpdateCategoryAsync(int id, Categories category); + Task DeleteCategoryAsync(int id); + Task > GetPagedCategoriesAsync(int pageNumber, int pageSize); +} + +public class CategoryService : ICategoryService +{ + private readonly GolfShopDbContext _context; + + public CategoryService(GolfShopDbContext context) + { + _context = context; + } + + public IQueryable GetAllCategoriesQuery() + { + return _context.Categories.Where(c => c.DeletedAt == null); + } + + public async Task GetCategoryByIdAsync(int id) + { + return await _context.Categories + .Where(c => c.DeletedAt == null) + .FirstOrDefaultAsync(c => c.Id == id); + + } + + public async Task CreateCategoryAsync(Categories category) + { + _context.Categories.Add(category); + await _context.SaveChangesAsync(); + return category; + } + + public async Task UpdateCategoryAsync(int id, Categories category) + { + var existingCategory = await _context.Categories.FindAsync(id); + if (existingCategory == null) + { + return; + } + existingCategory.Name = category.Name; + existingCategory.Description = category.Description; + await _context.SaveChangesAsync(); + } + + public async Task DeleteCategoryAsync(int id) + { + var category = await _context.Categories.FindAsync(id); + if (category == null) + { + return; + } + category.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + } + + public async Task> GetPagedCategoriesAsync(int pageNumber, int pageSize) + { + return await _context.Categories + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + } +} + + diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Services/IProductsService.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Services/IProductsService.cs new file mode 100644 index 0000000..faf530e --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Services/IProductsService.cs @@ -0,0 +1,83 @@ +using Microsoft.EntityFrameworkCore; +using Sills.GolfShop.eCommerceAPI.Data; +using Sills.GolfShop.eCommerceAPI.Models; + +namespace Sills.GolfShop.eCommerceAPI.Services; + + +public interface IProductsService +{ + Task> GetAllProductsAsync(); + Task GetProductByIdAsync(int id); + Task CreateProductAsync(Product product); + Task UpdateProductAsync(int id, Product product); + Task DeleteProductAsync(int id); + Task> GetPagedProductsAsync(int pageNumber, int pageSize); +} +public class ProductsService : IProductsService +{ + private readonly GolfShopDbContext _context; + + public ProductsService(GolfShopDbContext context) + { + _context = context; + } + + public async Task> GetAllProductsAsync() + { + return await _context.Products + .Where(p => p.DeletedAt == null) + .ToListAsync(); + } + + public async Task GetProductByIdAsync(int id) + { + return await _context.Products + .Where(p => p.DeletedAt == null) + .FirstOrDefaultAsync(p => p.Id == id); + } + + public async Task CreateProductAsync(Product product) + { + _context.Products.Add(product); + await _context.SaveChangesAsync(); + return product; + } + + public async Task UpdateProductAsync(int id, Product product) + { + var existingProduct = await _context.Products + .Where(p => p.DeletedAt == null) + .FirstOrDefaultAsync(p => p.Id == id); + if (existingProduct == null) + { + return; + } + existingProduct.Name = product.Name; + existingProduct.Description = product.Description; + existingProduct.Price = product.Price; + existingProduct.CategoryId = product.CategoryId; + await _context.SaveChangesAsync(); + } + + public async Task DeleteProductAsync(int id) + { + var product = await _context.Products.FindAsync(id); + if (product == null) + { + return; + } + product.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + } + + public async Task> GetPagedProductsAsync(int pageNumber, int pageSize) + { + return await _context.Products + .Where(p => p.DeletedAt == null) + .OrderBy(p => p.Id) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + } +} \ No newline at end of file diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Services/ISalesService.cs b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Services/ISalesService.cs new file mode 100644 index 0000000..6027764 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Services/ISalesService.cs @@ -0,0 +1,100 @@ +using Microsoft.EntityFrameworkCore; +using Sills.GolfShop.eCommerceAPI.Data; +using Sills.GolfShop.eCommerceAPI.Models; + +namespace Sills.GolfShop.eCommerceAPI.Services; + +public interface ISalesService +{ + Task> GetAllSalesAsync(); + Task GetSaleByIdAsync(int id); + Task CreateSaleAsync(Sales sale); + Task DeleteSaleAsync(int id); + Task UpdateSaleAsync(int id, Sales sale); + Task> GetPagedSalesAsync(int pageNumber, int pageSize); +} +public class SalesService : ISalesService +{ + private readonly GolfShopDbContext _context; + + public SalesService(GolfShopDbContext context) + { + _context = context; + } + + + public async Task> GetAllSalesAsync() + { + return await _context.Sales.ToListAsync(); + } + + public async Task GetSaleByIdAsync(int id) + { + return await _context.Sales.FindAsync(id); + } + public async Task CreateSaleAsync(Sales sale) + { + _context.Sales.Add(sale); + await _context.SaveChangesAsync(); + return sale; + } + public async Task UpdateSaleAsync(int id, Sales sale) + { + var existingSale = await _context.Sales.FindAsync(id); + if (existingSale == null) + { + return; + } + existingSale.customerName = sale.customerName; + existingSale.shippingAddress = sale.shippingAddress; + await _context.SaveChangesAsync(); + } + public async Task DeleteSaleAsync(int id) + { + var sale = await _context.Sales.FindAsync(id); + if (sale == null) + { + return; + } + _context.Sales.Remove(sale); + await _context.SaveChangesAsync(); + } + public async Task AddProductToSaleAsync(int saleId, int productId) + { + var sale = await _context.Sales.FindAsync(saleId); + var product = await _context.Products.FindAsync(productId); + if (sale == null || product == null) + { + return; + } + var productSale = new ProductSales + { + Sale = sale, + Product = product + }; + _context.ProductSales.Add(productSale); + await _context.SaveChangesAsync(); + } + + public async Task RemoveProductFromSaleAsync(int saleId, int productId) + { + var productSale = await _context.ProductSales + .FirstOrDefaultAsync(ps => ps.SaleID == saleId && ps.ProductID == productId); + if (productSale == null) + { + return; + } + _context.ProductSales.Remove(productSale); + await _context.SaveChangesAsync(); + } + + public async Task> GetPagedSalesAsync(int pageNumber, int pageSize) + { + return await _context.Sales + .OrderBy(s => s.Id) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + } + +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Sills.GolfShop.eCommerce.http b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Sills.GolfShop.eCommerce.http new file mode 100644 index 0000000..e69de29 diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Sills.GolfShop.eCommerceAPI.csproj b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Sills.GolfShop.eCommerceAPI.csproj new file mode 100644 index 0000000..eab98e1 --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/Sills.GolfShop.eCommerceAPI.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/appsettings.Development.json b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/appsettings.json b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/appsettings.json new file mode 100644 index 0000000..072d91f --- /dev/null +++ b/eCommerceApi.dsills735/Sills.GolfShop.eCommerce/appsettings.json @@ -0,0 +1,13 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=GolfShopDb;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "SeedingApiUrl": "https://localhost:8080/api/seed/bulk", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}