using LiteCharms.Features.Api.Configuration; using LiteCharms.Features.Hasher; using LiteCharms.Features.MidrandBooks.AuthorBooks; using LiteCharms.Features.MidrandBooks.Orders; using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Payments; using LiteCharms.Features.MidrandBooks.Payments.Models; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.JSInterop; using Microsoft.Extensions.Options; using System.Security.Claims; using System.Globalization; using LiteCharms; using Microsoft.AspNetCore.Components.Authorization; namespace MidrandBookshop.Components.Pages; public partial class Checkout() { [Inject] public HashService HashService { get; set; } = default!; [Inject] public PaymentService PaymentService { get; set; } = default!; [Inject] public OrderService OrderService { get; set; } = default!; [Inject] public BooksService BooksService { get; set; } = default!; [Inject] public CartService CartService { get; set; } = default!; [Inject] public PayfastService PayfastService { get; set; } = default!; [Inject] public IOptions PayfastOptions { get; set; } = default!; [Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = default!; [Inject] public IJSRuntime JSRuntime { get; set; } = default!; [Inject] private CancellationToken CancellationToken { get; set; } = default!; [Inject] public IToastService ToastService { get; set; } = default!; private Cart ShoppingCart => CartService.ShoppingCart; private ClaimsPrincipal? User { get; set; } private bool IsProcessing { get; set; } private decimal ShippingCost = 0; private bool IsSameAddress = true; public string? OrderNotes { get; set; } private Dictionary CheckoutPayload { get; set; } = []; // Tracks available quantities indexed by Price ID protected Dictionary AvailableStockMap { get; set; } = []; // Quick validation flag to evaluate checkout block state protected bool HasStockExceptions => ShoppingCart.Items.Any(item => AvailableStockMap.TryGetValue(item.Price!.Id, out var count) && count <= 0); protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); User = authState!.User; Navigation.LocationChanged += OnLocationChanged; CartService.OnCartChanged += CartService_OnCartChanged; if (CartService.ShoppingCart.Items.Count == 0) { await CartService.LoadCartFromStorageAsync(); } await RefreshStockValidationAsync(); } private async void CartService_OnCartChanged() { await RefreshStockValidationAsync(); await InvokeAsync(StateHasChanged); } private void OnLocationChanged(object? sender, LocationChangedEventArgs e) => StateHasChanged(); private async Task RefreshStockValidationAsync() { AvailableStockMap.Clear(); foreach (var item in ShoppingCart.Items) { if (item.Price is not null) { // Mapped fallback default (set to 0 for specific keys to test stock warnings instantly) // In production: pull from your inventory system: // var stockCheck = await BooksService.GetStockLevelAsync(item.Price.Id); int liveStockAvailable = 1; AvailableStockMap[item.Price.Id] = liveStockAvailable; } } } private async Task ChangeQuantity(CartItem item, int delta) { var peekQuantity = item.Quantity + delta; if (peekQuantity < 1) return; // Block internal counters exceeding live available thresholds if (AvailableStockMap.TryGetValue(item.Price!.Id, out var maxAvailable) && peekQuantity > maxAvailable) { ToastService.ShowWarning($"Cannot exceed remaining stock limit ({maxAvailable} available).", "Stock Limit"); return; } CartService.UpdateQuantity(item.Price!.Id, delta); await CartService.SaveCartToStorageAsync(); } private async Task RemoveFromCart(CartItem item) { CartService.RemoveOneItem(item.Price!.Id); await CartService.SaveCartToStorageAsync(); } private async Task PayNow(MouseEventArgs args) { // Fail-safe protection boundary check if (HasStockExceptions) { ToastService.ShowError("Your order cannot contain items that are out of stock.", "Inventory Issue"); return; } if (IsProcessing) { ToastService.ShowWarning("Please wait, completing your payment", "Busy..."); return; } try { IsProcessing = true; StateHasChanged(); Result orderResult; var customerId = (long)ShoppingCart.CustomerId!; if (!ShoppingCart.OrderId.HasValue) { CreateOrder request = new(ShoppingCart.TotalAmount, null); orderResult = await OrderService.CreateOrderAsync(customerId, request, CancellationToken); ShoppingCart.OrderId = orderResult.Value; } List orderItems = []; var orderId = (long)ShoppingCart.OrderId; await OrderService.ClearOrderItemsAsync(orderId, CancellationToken); foreach (var item in ShoppingCart.Items) { var bookRequest = await BooksService.GetBookByProductIdAsync(item.Price!.Id, CancellationToken); if (bookRequest.IsSuccess) { var orderItem = new CreateOrderItem(bookRequest.Value.Id, item.Price.Id, item.Quantity); orderItems.Add(orderItem); } } var orderHash = HashService.HashEncodeLongId(orderId).Value; var paymentGen = await PaymentService.CreatePaymentAsync(ShoppingCart.TotalAmount, orderId, orderHash, CancellationToken); long paymentId = 0; if (paymentGen.IsSuccess) paymentId = paymentGen.Value; if (paymentGen.IsFailed) { var paymentFetch = await PaymentService.GetOrderPaymentAsync(orderId, CancellationToken); if (paymentFetch.IsFailed) { ToastService.ShowError("Failed to fetch your previously made payment", "Payment Check"); IsProcessing = false; return; } paymentId = paymentFetch.Value.Id; } CreateLedgerEntry ledgerRequest = new() { OrderId = orderId, CustomerId = customerId, PaymentGatewayId = 1, PaymentGatewayReference = orderHash, PaymentId = paymentId, Status = LiteCharms.Features.LedgerStatuses.Sent, }; await PaymentService.WriteLedgerEntryAsync(ledgerRequest, CancellationToken); var addItemsResult = await OrderService.AddItemsToOrderAsync(orderId, [.. orderItems], CancellationToken); var hostAddress = Navigation.BaseUri.TrimEnd('/'); CheckoutPayload = new Dictionary { { "merchant_id", PayfastOptions.Value.MerchantId! }, { "merchant_key", PayfastOptions.Value.MerchantKey! }, { "return_url", $"{hostAddress}/payment-success?reference={orderHash}" }, { "cancel_url", $"{hostAddress}/payment-failed?reference={orderHash}" }, { "notify_url", "https://api.uat.midrandbooks.co.za/v1/payments/payfast/confirm" }, { "email_address", User?.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)!.Value! }, { "m_payment_id", orderHash }, { "amount", ShoppingCart.TotalAmount.ToString("F2", CultureInfo.InvariantCulture) }, { "item_name", "MidrandBooks Sale" }, }; var signature = PayfastService.GenerateSignature(CheckoutPayload!, PayfastOptions.Value.Passphrase).Value; CheckoutPayload.Add("signature", signature); StateHasChanged(); await JSRuntime.InvokeVoidAsync("eval", "document.getElementById('payfastForm').submit();"); } catch (Exception ex) { ToastService.ShowError($"Failed to perform checkout: {ex.Message}", "Checkout"); IsProcessing = false; StateHasChanged(); } } }