using DateTimeConverterCL; using FrontOffice.BFF.ShopingCart.Protobuf.Protos.ShopingCart; using Google.Protobuf.WellKnownTypes; namespace FrontOffice.Main.Utilities; public record CartItem(long cartId,long ProductId, string Title, string ImageUrl, long UnitPrice, int Quantity) { public long LineTotal => UnitPrice * Quantity; public long DiscountValue => (Discount*(UnitPrice * Quantity))/100; public int Discount { get; init; } public string Created { get; init; } = string.Empty; public string Description { get; init; } = string.Empty; } public class CartService { private readonly ShopingCartContract.ShopingCartContractClient _client; private readonly List _items = new(); public event Action? OnChange; public CartService(ShopingCartContract.ShopingCartContractClient client) { _client = client; _ = LoadFromServerAsync(); } public IReadOnlyList Items => _items.AsReadOnly(); public long Total => _items.Sum(i => i.LineTotal); public long TotalDiscount => _items.Sum(i => i.DiscountValue); public int Count => _items.Sum(i => i.Quantity); public async Task Add(Product product, int quantity = 1) { if (quantity <= 0) return; var existing = _items.FirstOrDefault(i => i.ProductId == product.Id); int newQuantity; if (existing is null) { newQuantity = quantity; _items.Add(new CartItem(0,product.Id, product.Title, product.ImageUrl, product.Price, newQuantity) { Discount = product.Discount, Created = DateTime.Now.MiladiToJalali(), Description = product.Description }); } else { var idx = _items.IndexOf(existing); newQuantity = existing.Quantity + quantity; _items[idx] = existing with { Quantity = newQuantity }; } Notify(); try { if (existing is null) { await _client.AddNewUserCartAsync(new AddNewUserCartRequest { ProductId = product.Id, Count = newQuantity }); await LoadFromServerAsync(); } else { await _client.UpdateUserCartAsync(new UpdateUserCartRequest { UserCartId = existing.cartId, Count = newQuantity }); } } catch { // Best-effort sync with backend; keep local state on failure } } public async Task UpdateQuantity(long productId, int quantity) { var existing = _items.FirstOrDefault(i => i.ProductId == productId); if (existing is null) return; if (quantity <= 0) { Remove(existing.cartId); return; } var idx = _items.IndexOf(existing); _items[idx] = existing with { Quantity = quantity }; Notify(); try { await _client.UpdateUserCartAsync(new UpdateUserCartRequest { UserCartId = existing.cartId, Count = quantity }); } catch { // Best-effort sync with backend; keep local state on failure } } public async Task Remove(long cartId) { _items.RemoveAll(i => i.cartId == cartId); Notify(); try { // Interpret Count=0 as remove from cart await _client.UpdateUserCartAsync(new UpdateUserCartRequest { UserCartId = cartId, Count = 0 }); } catch { // Best-effort sync with backend; keep local state on failure } } public async Task Clear() { var productIds = _items.Select(i => i.ProductId).ToList(); _items.Clear(); Notify(); try { foreach (var id in productIds) { await _client.UpdateUserCartAsync(new UpdateUserCartRequest { UserCartId = id, Count = 0 }); } } catch { // Best-effort sync with backend; keep local state on failure } } private void Notify() => OnChange?.Invoke(); private async Task LoadFromServerAsync() { try { var response = await _client.GetAllUserCartAsync(new Empty()); _items.Clear(); foreach (var model in response.Models) { var item = new CartItem( cartId:model.Id, ProductId: model.ProductId, Title: model.ProductTitle ?? string.Empty, ImageUrl: string.IsNullOrWhiteSpace(model.ProductThumbnailPath) ? string.Empty : UrlUtility.DownloadUrl + model.ProductThumbnailPath, UnitPrice: model.ProductPrice, Quantity: model.Count > 0 ? model.Count : 1) { Discount = model.ProductDiscount, Created = model.Created.ToDateTime().MiladiToJalali(), Description = model.ProductShortInfomation }; _items.Add(item); } Notify(); } catch { // If backend is unreachable or user is unauthenticated, fall back to local-only cart. } } }