Stable payment and order process
This commit is contained in:
@@ -19,7 +19,6 @@ public partial class Checkout()
|
|||||||
[Inject] public IOptions<PayfastSettings> PayfastOptions { get; set; } = default!;
|
[Inject] public IOptions<PayfastSettings> PayfastOptions { get; set; } = default!;
|
||||||
[Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = default!;
|
[Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = default!;
|
||||||
[Inject] public IJSRuntime JSRuntime { get; set; } = default!;
|
[Inject] public IJSRuntime JSRuntime { get; set; } = default!;
|
||||||
[Inject] private HydrationService HydrationService { get; set; } = default!;
|
|
||||||
[Inject] private CancellationToken CancellationToken { get; set; } = default!;
|
[Inject] private CancellationToken CancellationToken { get; set; } = default!;
|
||||||
|
|
||||||
private Cart ShoppingCart => CartService.ShoppingCart;
|
private Cart ShoppingCart => CartService.ShoppingCart;
|
||||||
@@ -38,19 +37,6 @@ public partial class Checkout()
|
|||||||
|
|
||||||
Navigation.LocationChanged += OnLocationChanged;
|
Navigation.LocationChanged += OnLocationChanged;
|
||||||
CartService.OnCartChanged += CartService_OnCartChanged;
|
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);
|
private async void CartService_OnCartChanged() => await InvokeAsync(StateHasChanged);
|
||||||
@@ -117,13 +103,26 @@ public partial class Checkout()
|
|||||||
var orderHash = HashService.HashEncodeLongId(orderId).Value;
|
var orderHash = HashService.HashEncodeLongId(orderId).Value;
|
||||||
var paymentGen = await PaymentService.CreatePaymentAsync(ShoppingCart.TotalAmount, orderId, orderHash, CancellationToken);
|
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()
|
CreateLedgerEntry ledgerRequest = new()
|
||||||
{
|
{
|
||||||
OrderId = orderId,
|
OrderId = orderId,
|
||||||
CustomerId = customerId,
|
CustomerId = customerId,
|
||||||
PaymentGatewayId = 1, // TODO: lookup value to match user selection
|
PaymentGatewayId = 1,
|
||||||
PaymentGatewayReference = orderHash,
|
PaymentGatewayReference = orderHash,
|
||||||
PaymentId = paymentGen.Value,
|
PaymentId = paymentId,
|
||||||
Status = LiteCharms.Features.LedgerStatuses.Sent,
|
Status = LiteCharms.Features.LedgerStatuses.Sent,
|
||||||
};
|
};
|
||||||
await PaymentService.WriteLedgerEntryAsync(ledgerRequest, CancellationToken);
|
await PaymentService.WriteLedgerEntryAsync(ledgerRequest, CancellationToken);
|
||||||
@@ -135,8 +134,8 @@ public partial class Checkout()
|
|||||||
{
|
{
|
||||||
{ "merchant_id", PayfastOptions.Value.MerchantId! },
|
{ "merchant_id", PayfastOptions.Value.MerchantId! },
|
||||||
{ "merchant_key", PayfastOptions.Value.MerchantKey! },
|
{ "merchant_key", PayfastOptions.Value.MerchantKey! },
|
||||||
{ "return_url", $"{hostAddress}/payment-success" },
|
{ "return_url", $"{hostAddress}/payment-success?reference={orderHash}" },
|
||||||
{ "cancel_url", $"{hostAddress}/payment-failed" },
|
{ "cancel_url", $"{hostAddress}/payment-failed?reference={orderHash}" },
|
||||||
{ "notify_url", "https://api.uat.midrandbooks.co.za/v1/payments/payfast/confirm" },
|
{ "notify_url", "https://api.uat.midrandbooks.co.za/v1/payments/payfast/confirm" },
|
||||||
{ "email_address", User?.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)!.Value! },
|
{ "email_address", User?.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)!.Value! },
|
||||||
{ "m_payment_id", orderHash },
|
{ "m_payment_id", orderHash },
|
||||||
|
|||||||
@@ -99,14 +99,18 @@ public partial class Home : ComponentBase
|
|||||||
|
|
||||||
private bool HasMoreItems => FilteredData.Count() > VisibleCount;
|
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)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender == false && HydrationService.CartHydrated == false)
|
if (firstRender == false && HydrationService.CartHydrated == false)
|
||||||
{
|
{
|
||||||
await HydrationService.EnsureCustomerExistsAsync(CancellationToken);
|
if(!CartService.ShoppingCart.CustomerId.HasValue)
|
||||||
await HydrationService.RehydrateCartFromPendingOrderAsync(CancellationToken);
|
await HydrationService.EnsureCustomerExistsAsync(CancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/payment-failed"
|
@page "/payment-failed"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
@inject NavigationManager Navigation
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
|
|
||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
@@ -13,18 +14,16 @@
|
|||||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="fw-bold mb-3">Payment Failed</h1>
|
<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, and your cart items are safe.</p>
|
<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">
|
<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 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>
|
</div>
|
||||||
|
|
||||||
<div class="d-grid gap-3 mt-5">
|
<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="row g-3">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<a href="/" class="btn btn-outline-dark w-100 rounded-pill py-3">View Store</a>
|
<a href="/" class="btn btn-outline-dark w-100 rounded-pill py-3">View Store</a>
|
||||||
@@ -35,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/payment-success"
|
@page "/payment-success"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
@inject NavigationManager Navigation
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
|
|
||||||
<div class="container py-5">
|
<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>
|
<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">
|
<div class="bg-light p-3 rounded mt-4">
|
||||||
<p class="mb-0 text-muted small text-uppercase fw-bold">Order Number</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -27,12 +28,12 @@
|
|||||||
<a href="/account" class="btn btn-outline-dark w-100 rounded-pill py-3">Order History</a>
|
<a href="/account" class="btn btn-outline-dark w-100 rounded-pill py-3">Order History</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,7 +65,7 @@ public sealed class HydrationService(AuthenticationStateProvider authStateProvid
|
|||||||
{
|
{
|
||||||
if (User?.Identity?.IsAuthenticated == false) return;
|
if (User?.Identity?.IsAuthenticated == false) return;
|
||||||
|
|
||||||
if (ShoppingCart.OrderId > 0 && ShoppingCart.CustomerId > 0)
|
if (ShoppingCart.OrderId.HasValue && ShoppingCart.CustomerId.HasValue)
|
||||||
{
|
{
|
||||||
cartService.CalculateTotalPrice();
|
cartService.CalculateTotalPrice();
|
||||||
CartHydrated = true;
|
CartHydrated = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user