Merge pull request 'cart' (#98) from cart into main

Reviewed-on: #98
This commit was merged in pull request #98.
This commit is contained in:
2026-06-16 13:47:36 +02:00
7 changed files with 176 additions and 32 deletions
@@ -19,7 +19,6 @@ public partial class Checkout()
[Inject] public IOptions<PayfastSettings> 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 },
@@ -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)
{
if(!CartService.ShoppingCart.CustomerId.HasValue)
await HydrationService.EnsureCustomerExistsAsync(CancellationToken);
await HydrationService.RehydrateCartFromPendingOrderAsync(CancellationToken);
}
}
@@ -1,5 +1,6 @@
@page "/payment-failed"
@rendermode InteractiveServer
@inject NavigationManager Navigation
@attribute [Authorize]
<div class="container py-5">
@@ -13,18 +14,16 @@
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
</div>
<h1 class="fw-bold mb-3">Payment Failed</h1>
<p class="text-muted fs-5">We couldn't process your transaction. Don't worry, no money was deducted from your account, and your cart items are safe.</p>
<h1 class="fw-bold mb-3">Payment Cancelled</h1>
<p class="text-muted fs-5">We couldn't process your transaction. Don't worry, no money was deducted from your account.</p>
<div class="bg-light p-3 rounded mt-4">
<p class="mb-0 text-muted small text-uppercase fw-bold">Common Causes</p>
<p class="mb-0 fs-6 text-dark mt-1">Insufficient funds, incorrect card details, or a temporary bank gateway timeout.</p>
<p class="mb-0 fs-6 text-dark mt-1">The order was cancelled / insufficient funds, incorrect card details, or a temporary bank gateway timeout.</p>
</div>
</div>
<div class="d-grid gap-3 mt-5">
<a href="/checkout" class="btn btn-dark btn-lg rounded-pill py-3">Try Again</a>
<div class="row g-3">
<div class="col-6">
<a href="/" class="btn btn-outline-dark w-100 rounded-pill py-3">View Store</a>
@@ -35,7 +34,7 @@
</div>
</div>
<p class="mt-5 text-muted small">If you noticed a charge or have any order questions, please contact our support desk with your account email <strong>user@email.com</strong>.</p>
<p class="mt-5 text-muted small">If you noticed a charge or have any order questions, please contact our support desk with your account email <strong>shop@litecharms.co.za</strong>.</p>
</div>
</div>
</div>
@@ -0,0 +1,72 @@
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");
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) 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();
}
}
@@ -1,5 +1,6 @@
@page "/payment-success"
@rendermode InteractiveServer
@inject NavigationManager Navigation
@attribute [Authorize]
<div class="container py-5">
@@ -16,7 +17,7 @@
<p class="text-muted fs-5">Thank you for shopping with Midrand Books. Your order has been received and is being processed.</p>
<div class="bg-light p-3 rounded mt-4">
<p class="mb-0 text-muted small text-uppercase fw-bold">Order Number</p>
<h5 class="fw-bold mb-0">#MB-2026-8834</h5>
<h5 class="fw-bold mb-0">@PaymentReference</h5>
</div>
</div>
@@ -27,12 +28,12 @@
<a href="/account" class="btn btn-outline-dark w-100 rounded-pill py-3">Order History</a>
</div>
<div class="col-6">
<a href="/track-order" class="btn btn-outline-dark w-100 rounded-pill py-3">Track Order</a>
<a href="/account" class="btn btn-outline-dark w-100 rounded-pill py-3">Track Order</a>
</div>
</div>
</div>
<p class="mt-5 text-muted small">You will receive a confirmation email shortly at <strong>user@email.com</strong>.</p>
<p class="mt-5 text-muted small">You will receive a confirmation email shortly at <strong>@CustomerEmail</strong>.</p>
</div>
</div>
</div>
@@ -0,0 +1,69 @@
using LiteCharms.Features;
using LiteCharms.Features.Hasher;
using LiteCharms.Features.MidrandBooks.Customers;
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 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");
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
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();
}
}
+1 -1
View File
@@ -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;