From d3e9b30be5cc9147d7c0842b4fe345a210197556 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 9 Jun 2026 20:51:56 +0200 Subject: [PATCH 1/2] Updated nuget packaged to includ the CartService --- MidrandBookshop/MidrandBookshop.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MidrandBookshop/MidrandBookshop.csproj b/MidrandBookshop/MidrandBookshop.csproj index 396b5d8..2bcaf5e 100644 --- a/MidrandBookshop/MidrandBookshop.csproj +++ b/MidrandBookshop/MidrandBookshop.csproj @@ -18,13 +18,13 @@ - + - + From 3bce80c96345f3433d5d0f86d355c37a8cea2c4b Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 9 Jun 2026 23:39:49 +0200 Subject: [PATCH 2/2] Implemented cart service with state tracker and linked to main layout --- .../Components/Layout/MainLayout.razor | 2 +- .../Components/Layout/MainLayout.razor.cs | 40 +++-- .../Components/Pages/Account.razor | 75 +-------- .../Components/Pages/Account.razor.cs | 75 +++++++++ .../Components/Pages/ProductView.razor.cs | 33 +++- MidrandBookshop/MidrandBookshop.csproj | 8 +- MidrandBookshop/Program.cs | 2 + .../Services/ShoppingCart/CartService.cs | 153 ++++++++++++++++++ .../Services/ShoppingCart/Models/Cart.cs | 18 +++ .../Services/ShoppingCart/Models/CartItem.cs | 12 ++ 10 files changed, 326 insertions(+), 92 deletions(-) create mode 100644 MidrandBookshop/Components/Pages/Account.razor.cs create mode 100644 MidrandBookshop/Services/ShoppingCart/CartService.cs create mode 100644 MidrandBookshop/Services/ShoppingCart/Models/Cart.cs create mode 100644 MidrandBookshop/Services/ShoppingCart/Models/CartItem.cs diff --git a/MidrandBookshop/Components/Layout/MainLayout.razor b/MidrandBookshop/Components/Layout/MainLayout.razor index a766e28..3090d41 100644 --- a/MidrandBookshop/Components/Layout/MainLayout.razor +++ b/MidrandBookshop/Components/Layout/MainLayout.razor @@ -158,7 +158,7 @@ @if (CartItems.Any()) { - @CartItems.Sum(i => i.Quantity) + @ShoppingCart.Items.Count } diff --git a/MidrandBookshop/Components/Layout/MainLayout.razor.cs b/MidrandBookshop/Components/Layout/MainLayout.razor.cs index db11298..015302b 100644 --- a/MidrandBookshop/Components/Layout/MainLayout.razor.cs +++ b/MidrandBookshop/Components/Layout/MainLayout.razor.cs @@ -1,11 +1,19 @@ -using Microsoft.AspNetCore.Components.Routing; -using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.WebUtilities; +using MidrandBookshop.Services.ShoppingCart; +using MidrandBookshop.Services.ShoppingCart.Models; namespace MidrandBookshop.Components.Layout; -public partial class MainLayout : IDisposable +public partial class MainLayout(CartService cartService) : IDisposable { + [Inject] + private AuthenticationStateProvider AuthStateProvider { get; set; } = default!; + + private Cart ShoppingCart => cartService.ShoppingCart; + + private AuthenticationState? AuthState { get; set; } + private System.Security.Claims.ClaimsPrincipal? User { get; set; } + private bool IsAuthenticated => User?.Identity?.IsAuthenticated ?? false; + private string SearchInputBuffer { get; set; } = string.Empty; private string GlobalSearchQuery { get; set; } = string.Empty; private bool IsSearchActive { get; set; } = false; @@ -18,19 +26,25 @@ public partial class MainLayout : IDisposable new CartItem { Id = 3, Title = "Album Architectures, Maputo", Author = "Guedes Archive", Price = 350, Quantity = 1 } }; - private void TriggerHeaderLogout() + protected override async Task OnInitializedAsync() { - // Force tear-down of the active client websocket pipeline safely - Navigation.NavigateTo("/logout", forceLoad: true); - } + AuthState = await AuthStateProvider.GetAuthenticationStateAsync(); + User = AuthState!.User; - protected override void OnInitialized() - { Navigation.LocationChanged += OnLocationChanged; + cartService.OnCartChanged += CartService_OnCartChanged; SyncSearchQueryFromUrl(); } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + await cartService.LoadCartFromStorageAsync(); + } + + private async void CartService_OnCartChanged() => await InvokeAsync(StateHasChanged); + private void OnLocationChanged(object? sender, LocationChangedEventArgs e) { SyncSearchQueryFromUrl(); @@ -107,7 +121,8 @@ public partial class MainLayout : IDisposable } private void RemoveFromCart(CartItem item) => CartItems.Remove(item); - private int GetCartTotal() => CartItems.Sum(item => item.Price * item.Quantity); + + private decimal GetCartTotal() => ShoppingCart?.TotalPrice ?? 0.00m; private void RedirectToCart() { @@ -124,6 +139,9 @@ public partial class MainLayout : IDisposable public void Dispose() { Navigation.LocationChanged -= OnLocationChanged; + cartService.OnCartChanged -= CartService_OnCartChanged; + + GC.SuppressFinalize(this); } public class CartItem diff --git a/MidrandBookshop/Components/Pages/Account.razor b/MidrandBookshop/Components/Pages/Account.razor index 0e55d84..b8dbb6a 100644 --- a/MidrandBookshop/Components/Pages/Account.razor +++ b/MidrandBookshop/Components/Pages/Account.razor @@ -225,77 +225,4 @@ - - -@code { - private bool showAddForm = false; - private AddressItem? editingAddress = null; - private string newAddressName = ""; - private string newStreetAddress = ""; - private string newCity = ""; - private string newPostalCode = ""; - private bool isBilling, isShipping; - - private List orderHistory = new() - { - new OrderItem { OrderId = "#MB-2026-9481", ProductId = "introduction-to-blazor", ProductTitle = "Introduction to Blazor WebAssembly Framework Development", OrderDate = new DateTime(2026, 5, 20), ShippingAddressName = "Home Address", Status = "Shipped", Total = 720.00 }, - new OrderItem { OrderId = "#MB-2026-8712", ProductId = "mastering-css-isolation", ProductTitle = "Mastering CSS Isolation in Modern .NET Web Applications Architecture", OrderDate = new DateTime(2026, 4, 14), ShippingAddressName = "Midrand Books Warehouse", Status = "Delivered", Total = 890.00 } - }; - - private List savedAddresses = new() - { - new AddressItem { Id = 1, Name = "Home Address", Street = "12 Main Road", City = "Midrand", PostalCode = "1685", IsBilling = true, IsShipping = true, IsPrimary = true }, - new AddressItem { Id = 2, Name = "Corporate Office", Street = "45 Challink Street", City = "Halfway House", PostalCode = "1682", IsBilling = true, IsShipping = false, IsPrimary = false }, - new AddressItem { Id = 3, Name = "Midrand Books Warehouse", Street = "Unit 8, Corporate Park North", City = "Randjespark", PostalCode = "1683", IsBilling = false, IsShipping = true, IsPrimary = false } - }; - - private void TriggerLogout() => Navigation.NavigateTo("/logout", forceLoad: true); - private void DownloadInvoice(string orderId) { /* Handle download sequence here */ } - private void OpenAddForm() { editingAddress = null; showAddForm = true; } - - private void SaveAddress() - { - if (!string.IsNullOrWhiteSpace(newAddressName) && !string.IsNullOrWhiteSpace(newStreetAddress)) - { - var nextId = savedAddresses.Any() ? savedAddresses.Max(a => a.Id) + 1 : 1; - savedAddresses.Add(new AddressItem - { - Id = nextId, - Name = newAddressName, - Street = newStreetAddress, - City = newCity, - PostalCode = newPostalCode, - IsBilling = isBilling, - IsShipping = isShipping, - IsPrimary = !savedAddresses.Any() - }); - ResetAddForm(); - } - } - - private void ResetAddForm() { newAddressName = ""; newStreetAddress = ""; newCity = ""; newPostalCode = ""; isBilling = isShipping = showAddForm = false; } - private void StartEditing(AddressItem addr) { showAddForm = false; editingAddress = new AddressItem { Id = addr.Id, Name = addr.Name, Street = addr.Street, City = addr.City, PostalCode = addr.PostalCode, IsBilling = addr.IsBilling, IsShipping = addr.IsShipping, IsPrimary = addr.IsPrimary }; } - private void CancelEditing() => editingAddress = null; - - private void UpdateAddress() - { - if (editingAddress != null) - { - var target = savedAddresses.FirstOrDefault(a => a.Id == editingAddress.Id); - if (target != null) { target.Name = editingAddress.Name; target.Street = editingAddress.Street; target.City = editingAddress.City; target.PostalCode = editingAddress.PostalCode; target.IsBilling = editingAddress.IsBilling; target.IsShipping = editingAddress.IsShipping; } - editingAddress = null; - } - } - - private void DeleteAddress(AddressItem addr) { if (editingAddress?.Id == addr.Id) editingAddress = null; savedAddresses.Remove(addr); if (addr.IsPrimary && savedAddresses.Any()) savedAddresses.First().IsPrimary = true; } - - private void SetPrimary(AddressItem target, ChangeEventArgs e) - { - var isChecked = (bool)(e.Value ?? false); - if (isChecked) { foreach (var addr in savedAddresses) addr.IsPrimary = (addr.Id == target.Id); } - else target.IsPrimary = false; - } - - public class AddressItem { public int Id { get; set; } public string Name { get; set; } = ""; public string Street { get; set; } = ""; public string City { get; set; } = ""; public string PostalCode { get; set; } = ""; public bool IsBilling { get; set; } public bool IsShipping { get; set; } public bool IsPrimary { get; set; } } - public class OrderItem { public string OrderId { get; set; } = ""; public string ProductId { get; set; } = ""; public string ProductTitle { get; set; } = ""; public DateTime OrderDate { get; set; } public string ShippingAddressName { get; set; } = ""; public string Status { get; set; } = ""; public double Total { get; set; } } -} \ No newline at end of file + \ No newline at end of file diff --git a/MidrandBookshop/Components/Pages/Account.razor.cs b/MidrandBookshop/Components/Pages/Account.razor.cs new file mode 100644 index 0000000..52416b7 --- /dev/null +++ b/MidrandBookshop/Components/Pages/Account.razor.cs @@ -0,0 +1,75 @@ +namespace MidrandBookshop.Components.Pages; + +public partial class Account +{ + private bool showAddForm = false; + private AddressItem? editingAddress = null; + private string newAddressName = ""; + private string newStreetAddress = ""; + private string newCity = ""; + private string newPostalCode = ""; + private bool isBilling, isShipping; + + private List orderHistory = new() + { + new OrderItem { OrderId = "#MB-2026-9481", ProductId = "introduction-to-blazor", ProductTitle = "Introduction to Blazor WebAssembly Framework Development", OrderDate = new DateTime(2026, 5, 20), ShippingAddressName = "Home Address", Status = "Shipped", Total = 720.00 }, + new OrderItem { OrderId = "#MB-2026-8712", ProductId = "mastering-css-isolation", ProductTitle = "Mastering CSS Isolation in Modern .NET Web Applications Architecture", OrderDate = new DateTime(2026, 4, 14), ShippingAddressName = "Midrand Books Warehouse", Status = "Delivered", Total = 890.00 } + }; + + private List savedAddresses = new() + { + new AddressItem { Id = 1, Name = "Home Address", Street = "12 Main Road", City = "Midrand", PostalCode = "1685", IsBilling = true, IsShipping = true, IsPrimary = true }, + new AddressItem { Id = 2, Name = "Corporate Office", Street = "45 Challink Street", City = "Halfway House", PostalCode = "1682", IsBilling = true, IsShipping = false, IsPrimary = false }, + new AddressItem { Id = 3, Name = "Midrand Books Warehouse", Street = "Unit 8, Corporate Park North", City = "Randjespark", PostalCode = "1683", IsBilling = false, IsShipping = true, IsPrimary = false } + }; + + private void TriggerLogout() => Navigation.NavigateTo("/logout", forceLoad: true); + private void DownloadInvoice(string orderId) { /* Handle download sequence here */ } + private void OpenAddForm() { editingAddress = null; showAddForm = true; } + + private void SaveAddress() + { + if (!string.IsNullOrWhiteSpace(newAddressName) && !string.IsNullOrWhiteSpace(newStreetAddress)) + { + var nextId = savedAddresses.Any() ? savedAddresses.Max(a => a.Id) + 1 : 1; + savedAddresses.Add(new AddressItem + { + Id = nextId, + Name = newAddressName, + Street = newStreetAddress, + City = newCity, + PostalCode = newPostalCode, + IsBilling = isBilling, + IsShipping = isShipping, + IsPrimary = !savedAddresses.Any() + }); + ResetAddForm(); + } + } + + private void ResetAddForm() { newAddressName = ""; newStreetAddress = ""; newCity = ""; newPostalCode = ""; isBilling = isShipping = showAddForm = false; } + private void StartEditing(AddressItem addr) { showAddForm = false; editingAddress = new AddressItem { Id = addr.Id, Name = addr.Name, Street = addr.Street, City = addr.City, PostalCode = addr.PostalCode, IsBilling = addr.IsBilling, IsShipping = addr.IsShipping, IsPrimary = addr.IsPrimary }; } + private void CancelEditing() => editingAddress = null; + + private void UpdateAddress() + { + if (editingAddress != null) + { + var target = savedAddresses.FirstOrDefault(a => a.Id == editingAddress.Id); + if (target != null) { target.Name = editingAddress.Name; target.Street = editingAddress.Street; target.City = editingAddress.City; target.PostalCode = editingAddress.PostalCode; target.IsBilling = editingAddress.IsBilling; target.IsShipping = editingAddress.IsShipping; } + editingAddress = null; + } + } + + private void DeleteAddress(AddressItem addr) { if (editingAddress?.Id == addr.Id) editingAddress = null; savedAddresses.Remove(addr); if (addr.IsPrimary && savedAddresses.Any()) savedAddresses.First().IsPrimary = true; } + + private void SetPrimary(AddressItem target, ChangeEventArgs e) + { + var isChecked = (bool)(e.Value ?? false); + if (isChecked) { foreach (var addr in savedAddresses) addr.IsPrimary = (addr.Id == target.Id); } + else target.IsPrimary = false; + } + + public class AddressItem { public int Id { get; set; } public string Name { get; set; } = ""; public string Street { get; set; } = ""; public string City { get; set; } = ""; public string PostalCode { get; set; } = ""; public bool IsBilling { get; set; } public bool IsShipping { get; set; } public bool IsPrimary { get; set; } } + public class OrderItem { public string OrderId { get; set; } = ""; public string ProductId { get; set; } = ""; public string ProductTitle { get; set; } = ""; public DateTime OrderDate { get; set; } public string ShippingAddressName { get; set; } = ""; public string Status { get; set; } = ""; public double Total { get; set; } } +} diff --git a/MidrandBookshop/Components/Pages/ProductView.razor.cs b/MidrandBookshop/Components/Pages/ProductView.razor.cs index 09090e3..dcafa21 100644 --- a/MidrandBookshop/Components/Pages/ProductView.razor.cs +++ b/MidrandBookshop/Components/Pages/ProductView.razor.cs @@ -3,6 +3,7 @@ using LiteCharms.Features.MidrandBooks.Authors; using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Products; using LiteCharms.Features.MidrandBooks.Products.Models; +using MidrandBookshop.Services.ShoppingCart; namespace MidrandBookshop.Components.Pages; @@ -12,10 +13,12 @@ public partial class ProductView : ComponentBase [Inject] private ProductService ProductService { get; set; } = default!; [Inject] private AuthorService AuthorService { get; set; } = default!; + [Inject] private CartService CartService { get; set; } = default!; [Inject] private NavigationManager Navigation { get; set; } = default!; protected bool IsLoading { get; private set; } = true; protected Product? CurrentProduct { get; private set; } + protected ProductPrice? CurrentPrice { get; private set; } protected decimal LivePrice { get; private set; } = 0.00m; protected string AuthorName { get; private set; } = "Unknown Author"; protected string PrimaryCategory { get; private set; } = "General"; @@ -44,6 +47,7 @@ public partial class ProductView : ComponentBase var priceResult = await ProductService.GetProductPriceAsync(BookId); LivePrice = priceResult.IsSuccess ? priceResult.Value.Amount : 0m; + CurrentPrice = priceResult.IsSuccess ? priceResult.Value : null; var categoryResult = await ProductService.GetProductCategoriesAsync(BookId); if (categoryResult.IsSuccess && categoryResult.Value.Length > 0) @@ -73,6 +77,7 @@ public partial class ProductView : ComponentBase catch (Exception) { CurrentProduct = null; + CurrentPrice = null; } finally { @@ -80,14 +85,34 @@ public partial class ProductView : ComponentBase } } - protected void IncreaseQty() => Quantity++; - protected void DecreaseQty() { if (Quantity > 1) Quantity--; } + protected void IncreaseQty() + { + Quantity++; - protected void HandleAddToCart() + if (CurrentPrice is not null) + CartService.UpdateQuantity(CurrentPrice!.Id, Quantity); + } + protected void DecreaseQty() + { + if (Quantity > 1) + { + Quantity--; + + CartService.UpdateQuantity(CurrentPrice!.Id, Quantity); + } + } + + protected async void HandleAddToCart() { if (CurrentProduct == null) return; + + if (CurrentPrice is not null) + { + CartService.AddItem(CurrentPrice); + Quantity++; + } } - + protected void ViewAllAuthorBooks() { if (CurrentAuthor is not null) diff --git a/MidrandBookshop/MidrandBookshop.csproj b/MidrandBookshop/MidrandBookshop.csproj index 2bcaf5e..f9c9cce 100644 --- a/MidrandBookshop/MidrandBookshop.csproj +++ b/MidrandBookshop/MidrandBookshop.csproj @@ -18,13 +18,13 @@ - + - + @@ -51,6 +51,10 @@ + + + + diff --git a/MidrandBookshop/Program.cs b/MidrandBookshop/Program.cs index 1172d23..0c2f01b 100644 --- a/MidrandBookshop/Program.cs +++ b/MidrandBookshop/Program.cs @@ -3,6 +3,7 @@ using LiteCharms.Features.Mediator; using LiteCharms.Features.MidrandBooks.Extensions; using Microsoft.AspNetCore.HttpOverrides; using MidrandBookshop.Components; +using MidrandBookshop.Services.ShoppingCart; using static LiteCharms.Features.Extensions.Quartz; var builder = WebApplication.CreateBuilder(args); @@ -26,6 +27,7 @@ builder.Services.AddEmailServiceBus(); builder.Services.AddHttpClient(); builder.Services.AddShopServices(); +builder.Services.AddScoped(); builder.Services.AddHashServices(builder.Configuration); builder.Services.AddMidrandShopDatabase(builder.Configuration); diff --git a/MidrandBookshop/Services/ShoppingCart/CartService.cs b/MidrandBookshop/Services/ShoppingCart/CartService.cs new file mode 100644 index 0000000..0f84fb6 --- /dev/null +++ b/MidrandBookshop/Services/ShoppingCart/CartService.cs @@ -0,0 +1,153 @@ +using LiteCharms.Features.Browser; +using LiteCharms.Features.Hasher; +using LiteCharms.Features.MidrandBooks.Products.Models; +using MidrandBookshop.Services.ShoppingCart.Models; + +namespace MidrandBookshop.Services.ShoppingCart; + +public sealed class CartService(LocalStorageService localStorage) +{ + private readonly string CartStorageKey = HashService.ToMd5Hash(nameof(Cart)).Value; + + public Cart ShoppingCart { get; private set; } = new(); + + public event Action? OnCartChanged; + + public Cart GetCart() => ShoppingCart; + + public void NotifyStateChanged() => OnCartChanged?.Invoke(); + + public async Task LoadCartFromStorageAsync() + { + var loadResult = await localStorage.GetAsync(CartStorageKey); + + if (loadResult.IsFailed) await localStorage.SaveAsync(CartStorageKey, ShoppingCart); + + if (loadResult.IsSuccess) ShoppingCart = loadResult.Value; + + NotifyStateChanged(); + } + + public async Task SaveCartToStorageAsync() => await localStorage.SaveAsync(CartStorageKey, ShoppingCart); + + public void AddItem(ProductPrice productPrice) + { + var itemExists = false; + + for (var i = 0; i < ShoppingCart.Items.Count; i++) + { + if (ShoppingCart.Items[i].Price!.Id == productPrice.Id) + { + ShoppingCart.Items[i].Quantity++; + ShoppingCart.Items[i].Amount += productPrice.Amount; + + itemExists = true; + + break; + } + } + + if (!itemExists) + ShoppingCart.Items.Add(new CartItem + { + Price = productPrice, + Amount = productPrice.Amount, + Quantity = 1, + }); + + CalculateTotalPrice(); + NotifyStateChanged(); + } + + public void UpdateQuantity(long productPriceId, int newQuantity) + { + if (newQuantity <= 0) + { + RemoveAllSameItem(productPriceId); + NotifyStateChanged(); + + return; + } + + for (var i = 0; i < ShoppingCart.Items.Count; i++) + { + if (ShoppingCart.Items[i].Price!.Id == productPriceId) + { + var oldQuantity = ShoppingCart.Items[i].Quantity; + var pricePerUnit = ShoppingCart.Items[i].Price!.Amount; + + ShoppingCart.Items[i].Quantity = newQuantity; + ShoppingCart.Items[i].Amount = pricePerUnit * newQuantity; + break; + } + } + + CalculateTotalPrice(); + NotifyStateChanged(); + } + + public void RemoveOneItem(long productPriceId) + { + for (var i = 0; i < ShoppingCart.Items.Count; i++) + { + if (ShoppingCart.Items[i].Price!.Id == productPriceId) + { + if (ShoppingCart.Items[i].Quantity <= 1) + { + ShoppingCart.Items.RemoveAt(i); + } + else + { + ShoppingCart.Items[i].Quantity--; + ShoppingCart.Items[i].Amount -= ShoppingCart.Items[i].Price!.Amount; + } + + break; + } + } + + CalculateTotalPrice(); + NotifyStateChanged(); + } + + public void RemoveAllSameItem(long productPriceId) + { + if (ShoppingCart.Items.Count == 0) return; + + var item = ShoppingCart.Items.FirstOrDefault(i => i.Price?.Id == productPriceId); + + if (item is not null) ShoppingCart.Items.Remove(item); + + CalculateTotalPrice(); + NotifyStateChanged(); + } + + public void Clear() + { + if(ShoppingCart.CustomerId is not null || ShoppingCart.OrderId is not null) + { + ShoppingCart.TotalPrice = 0; + ShoppingCart.TotalVat = 0; + ShoppingCart.Items.Clear(); + + return; + } + + ShoppingCart = new Cart(); + + NotifyStateChanged(); + } + + public decimal CalculateTotalPrice() + { + if (ShoppingCart.Items.Count == 0) return 0; + + var gross = ShoppingCart.Items.Sum(i => i.Amount); + + if (!ShoppingCart.IsVatInclusive) ShoppingCart.TotalVat = gross * ShoppingCart.VatRate; + + ShoppingCart.TotalPrice = gross + ShoppingCart.TotalVat; + + return ShoppingCart.TotalPrice; + } +} diff --git a/MidrandBookshop/Services/ShoppingCart/Models/Cart.cs b/MidrandBookshop/Services/ShoppingCart/Models/Cart.cs new file mode 100644 index 0000000..cc9f043 --- /dev/null +++ b/MidrandBookshop/Services/ShoppingCart/Models/Cart.cs @@ -0,0 +1,18 @@ +namespace MidrandBookshop.Services.ShoppingCart.Models; + +public sealed class Cart +{ + public long? CustomerId { get; set; } + + public long? OrderId { get; set; } + + public decimal TotalPrice { get; set; } + + public decimal TotalVat { get; set; } + + public decimal VatRate { get; set; } = 0.15m; + + public bool IsVatInclusive { get; set; } = true; + + public IList Items { get; set; } = []; +} diff --git a/MidrandBookshop/Services/ShoppingCart/Models/CartItem.cs b/MidrandBookshop/Services/ShoppingCart/Models/CartItem.cs new file mode 100644 index 0000000..3c891d9 --- /dev/null +++ b/MidrandBookshop/Services/ShoppingCart/Models/CartItem.cs @@ -0,0 +1,12 @@ +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace MidrandBookshop.Services.ShoppingCart.Models; + +public sealed class CartItem +{ + public ProductPrice? Price { get; set; } + + public long Quantity { get; set; } + + public decimal Amount { get; set; } +}