From 6ca781759f2b1104bf12fe6d8873d7fc2ee0280d Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 16 Jun 2026 13:39:40 +0200 Subject: [PATCH] Stable payment and order process --- .../Components/Pages/Checkout.razor.cs | 35 +++++----- .../Components/Pages/Home.razor.cs | 10 ++- .../Components/Pages/PaymentFailed.razor | 13 ++-- .../Components/Pages/PaymentFailed.razor.cs | 69 +++++++++++++++++++ .../Components/Pages/PaymentSuccess.razor | 7 +- .../Components/Pages/PaymentSuccess.razor.cs | 66 ++++++++++++++++++ MidrandBookshop/HydrationService.cs | 2 +- 7 files changed, 170 insertions(+), 32 deletions(-) create mode 100644 MidrandBookshop/Components/Pages/PaymentFailed.razor.cs create mode 100644 MidrandBookshop/Components/Pages/PaymentSuccess.razor.cs diff --git a/MidrandBookshop/Components/Pages/Checkout.razor.cs b/MidrandBookshop/Components/Pages/Checkout.razor.cs index ce4f920..d086e28 100644 --- a/MidrandBookshop/Components/Pages/Checkout.razor.cs +++ b/MidrandBookshop/Components/Pages/Checkout.razor.cs @@ -19,7 +19,6 @@ public partial class Checkout() [Inject] public IOptions PayfastOptions { get; set; } = default!; [Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = default!; [Inject] public IJSRuntime JSRuntime { get; set; } = default!; - [Inject] private HydrationService HydrationService { get; set; } = default!; [Inject] private CancellationToken CancellationToken { get; set; } = default!; private Cart ShoppingCart => CartService.ShoppingCart; @@ -38,19 +37,6 @@ public partial class Checkout() Navigation.LocationChanged += OnLocationChanged; CartService.OnCartChanged += CartService_OnCartChanged; - - await CartService.LoadCartFromStorageAsync(); - } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender == false && HydrationService.CartHydrated == false) - { - await HydrationService.EnsureCustomerExistsAsync(CancellationToken); - await HydrationService.RehydrateCartFromPendingOrderAsync(CancellationToken); - - CartService.NotifyStateChanged(); - } } private async void CartService_OnCartChanged() => await InvokeAsync(StateHasChanged); @@ -117,13 +103,26 @@ public partial class Checkout() 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) return; + + paymentId = paymentFetch.Value.Id; + } + CreateLedgerEntry ledgerRequest = new() { OrderId = orderId, CustomerId = customerId, - PaymentGatewayId = 1, // TODO: lookup value to match user selection + PaymentGatewayId = 1, PaymentGatewayReference = orderHash, - PaymentId = paymentGen.Value, + PaymentId = paymentId, Status = LiteCharms.Features.LedgerStatuses.Sent, }; await PaymentService.WriteLedgerEntryAsync(ledgerRequest, CancellationToken); @@ -135,8 +134,8 @@ public partial class Checkout() { { "merchant_id", PayfastOptions.Value.MerchantId! }, { "merchant_key", PayfastOptions.Value.MerchantKey! }, - { "return_url", $"{hostAddress}/payment-success" }, - { "cancel_url", $"{hostAddress}/payment-failed" }, + { "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 }, diff --git a/MidrandBookshop/Components/Pages/Home.razor.cs b/MidrandBookshop/Components/Pages/Home.razor.cs index 65598a6..52b6459 100644 --- a/MidrandBookshop/Components/Pages/Home.razor.cs +++ b/MidrandBookshop/Components/Pages/Home.razor.cs @@ -99,14 +99,18 @@ public partial class Home : ComponentBase private bool HasMoreItems => FilteredData.Count() > VisibleCount; - protected override async Task OnInitializedAsync() => await CartService.LoadCartFromStorageAsync(); + protected override async Task OnInitializedAsync() + { + if (CartService.ShoppingCart.Items.Count == 0) + await CartService.LoadCartFromStorageAsync(); + } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender == false && HydrationService.CartHydrated == false) { - await HydrationService.EnsureCustomerExistsAsync(CancellationToken); - await HydrationService.RehydrateCartFromPendingOrderAsync(CancellationToken); + if(!CartService.ShoppingCart.CustomerId.HasValue) + await HydrationService.EnsureCustomerExistsAsync(CancellationToken); } } diff --git a/MidrandBookshop/Components/Pages/PaymentFailed.razor b/MidrandBookshop/Components/Pages/PaymentFailed.razor index 4750825..7f51f1e 100644 --- a/MidrandBookshop/Components/Pages/PaymentFailed.razor +++ b/MidrandBookshop/Components/Pages/PaymentFailed.razor @@ -1,5 +1,6 @@ @page "/payment-failed" @rendermode InteractiveServer +@inject NavigationManager Navigation @attribute [Authorize]
@@ -13,18 +14,16 @@
-

Payment Failed

-

We couldn't process your transaction. Don't worry, no money was deducted from your account, and your cart items are safe.

+

Payment Cancelled

+

We couldn't process your transaction. Don't worry, no money was deducted from your account.

Common Causes

-

Insufficient funds, incorrect card details, or a temporary bank gateway timeout.

+

The order was cancelled / insufficient funds, incorrect card details, or a temporary bank gateway timeout.

-
- Try Again - +
View Store @@ -35,7 +34,7 @@
-

If you noticed a charge or have any order questions, please contact our support desk with your account email user@email.com.

+

If you noticed a charge or have any order questions, please contact our support desk with your account email shop@litecharms.co.za.

\ No newline at end of file diff --git a/MidrandBookshop/Components/Pages/PaymentFailed.razor.cs b/MidrandBookshop/Components/Pages/PaymentFailed.razor.cs new file mode 100644 index 0000000..fe5d385 --- /dev/null +++ b/MidrandBookshop/Components/Pages/PaymentFailed.razor.cs @@ -0,0 +1,69 @@ +using LiteCharms.Features; +using LiteCharms.Features.Hasher; +using LiteCharms.Features.MidrandBooks.Customers; +using LiteCharms.Features.MidrandBooks.Orders; +using LiteCharms.Features.MidrandBooks.Payments; +using LiteCharms.Features.MidrandBooks.Payments.Models; + +namespace MidrandBookshop.Components.Pages; + +public partial class PaymentFailed +{ + [Inject] public CartService CartService { get; set; } = default!; + [Inject] public OrderService OrderService { get; set; } = default!; + [Inject] private CustomerService CustomerService { get; set; } = default!; + [Inject] public PaymentService PaymentService { get; set; } = default!; + [Inject] public HashService HashService { get; set; } = default!; + [Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = default!; + [Inject] private CancellationToken CancellationToken { get; set; } = default!; + + private ClaimsPrincipal? User { get; set; } + + [Parameter] + [SupplyParameterFromQuery(Name = "reference")] + public string? PaymentReference { get; set; } + + protected override async Task OnInitializedAsync() + { + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + User = authState!.User; + + if (User?.Identity?.IsAuthenticated == false) Navigation.NavigateTo("/login"); + + if (string.IsNullOrWhiteSpace(PaymentReference)) return; + + long orderId = HashService.DecodeLongIdHash(PaymentReference).Value; + + var customerEmail = User!.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)!.Value!; + + var customerFetch = await CustomerService.GetCustomerAsync(customerEmail, CancellationToken); + + if (customerFetch.IsFailed) return; + + long customerId = customerFetch.Value.Id; + + var orderUpdateResult = await OrderService.UpdateOrderStatusAsync(orderId, OrderStatus.Cancelled, CancellationToken); + + if (orderUpdateResult.IsFailed) return; + + var paymentIdFetch = await PaymentService.GetOrderPaymentAsync(orderId, CancellationToken); + + if (paymentIdFetch.IsFailed) return; + + await PaymentService.WriteLedgerEntryAsync(new CreateLedgerEntry + { + CustomerId = customerId, + OrderId = orderId, + PaymentGatewayId = 1, + PaymentGatewayReference = PaymentReference, + PaymentId = paymentIdFetch.Value.Id, + Status = LedgerStatuses.Cancelled + + }, CancellationToken); + + CartService.Clear(); + CartService.ShoppingCart.OrderId = null; + await CartService.SaveCartToStorageAsync(); + CartService.NotifyStateChanged(); + } +} diff --git a/MidrandBookshop/Components/Pages/PaymentSuccess.razor b/MidrandBookshop/Components/Pages/PaymentSuccess.razor index ebd109d..bbe340c 100644 --- a/MidrandBookshop/Components/Pages/PaymentSuccess.razor +++ b/MidrandBookshop/Components/Pages/PaymentSuccess.razor @@ -1,5 +1,6 @@ @page "/payment-success" @rendermode InteractiveServer +@inject NavigationManager Navigation @attribute [Authorize]
@@ -16,7 +17,7 @@

Thank you for shopping with Midrand Books. Your order has been received and is being processed.

Order Number

-
#MB-2026-8834
+
@PaymentReference
@@ -27,12 +28,12 @@ Order History -

You will receive a confirmation email shortly at user@email.com.

+

You will receive a confirmation email shortly at @CustomerEmail.

\ No newline at end of file diff --git a/MidrandBookshop/Components/Pages/PaymentSuccess.razor.cs b/MidrandBookshop/Components/Pages/PaymentSuccess.razor.cs new file mode 100644 index 0000000..a1668ff --- /dev/null +++ b/MidrandBookshop/Components/Pages/PaymentSuccess.razor.cs @@ -0,0 +1,66 @@ +using LiteCharms.Features; +using LiteCharms.Features.Hasher; +using LiteCharms.Features.MidrandBooks.Customers; +using LiteCharms.Features.MidrandBooks.Orders; +using LiteCharms.Features.MidrandBooks.Payments; +using LiteCharms.Features.MidrandBooks.Payments.Models; + +namespace MidrandBookshop.Components.Pages; + +public partial class PaymentSuccess +{ + [Inject] private CartService CartService { get; set; } = default!; + [Inject] private OrderService OrderService { get; set; } = default!; + [Inject] private CustomerService CustomerService { get; set; } = default!; + [Inject] private PaymentService PaymentService { get; set; } = default!; + [Inject] private HashService HashService { get; set; } = default!; + [Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = default!; + [Inject] private CancellationToken CancellationToken { get; set; } = default!; + + [Parameter] + [SupplyParameterFromQuery(Name = "reference")] + public string? PaymentReference { get; set; } + + private ClaimsPrincipal? User { get; set; } + + private string? CustomerEmail { get; set; } + + protected override async Task OnInitializedAsync() + { + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + User = authState!.User; + + if (User?.Identity?.IsAuthenticated == false) Navigation.NavigateTo("/login"); + + long orderId = HashService.DecodeLongIdHash(PaymentReference!).Value; + string orderHash = HashService.HashEncodeLongId(orderId).Value; + + CustomerEmail = User!.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)!.Value!; + + var customerFetch = await CustomerService.GetCustomerAsync(CustomerEmail, CancellationToken); + + if (customerFetch.IsFailed) return; + + long customerId = customerFetch.Value.Id; + + var paymentIdFetch = await PaymentService.GetOrderPaymentAsync(orderId, CancellationToken); + + if (paymentIdFetch.IsFailed) return; + + await PaymentService.WriteLedgerEntryAsync(new CreateLedgerEntry + { + CustomerId = customerId, + OrderId = orderId, + PaymentGatewayId = 1, + PaymentGatewayReference = orderHash, + PaymentId = paymentIdFetch.Value.Id, + Status = LedgerStatuses.Changed + + }, CancellationToken); + + CartService.Clear(); + CartService.ShoppingCart.OrderId = null; + await CartService.SaveCartToStorageAsync(); + CartService.NotifyStateChanged(); + } +} diff --git a/MidrandBookshop/HydrationService.cs b/MidrandBookshop/HydrationService.cs index ca531c4..24cea95 100644 --- a/MidrandBookshop/HydrationService.cs +++ b/MidrandBookshop/HydrationService.cs @@ -65,7 +65,7 @@ public sealed class HydrationService(AuthenticationStateProvider authStateProvid { if (User?.Identity?.IsAuthenticated == false) return; - if (ShoppingCart.OrderId > 0 && ShoppingCart.CustomerId > 0) + if (ShoppingCart.OrderId.HasValue && ShoppingCart.CustomerId.HasValue) { cartService.CalculateTotalPrice(); CartHydrated = true;