diff --git a/docs/CUSTOMER-SERVICES.md b/docs/CUSTOMER-SERVICES.md index a7031de..05ff8bd 100644 --- a/docs/CUSTOMER-SERVICES.md +++ b/docs/CUSTOMER-SERVICES.md @@ -67,10 +67,12 @@ | 4.5 | `WalletService.VerifyMagicChargeAsync` | `UserWalletContract.VerifyMagicCharge` | `/profile/payment-callback?type=magic-wallet` | تأیید شارژ Magic پس از درگاه | | 4.6 | `WalletService.InitiateDiscountChargeAsync` | `UserWalletContract.InitiateDiscountCharge` | `/profile/charge-discount-wallet` | شروع شارژ کیف پول تخفیفی → درگاه | | 4.7 | `WalletService.VerifyDiscountChargeAsync` | `UserWalletContract.VerifyDiscountCharge` | `/profile/payment-callback?type=discount-wallet` | تأیید شارژ کیف تخفیفی | -| 4.8 | `WalletService.GetWithdrawalsAsync` | `UserWalletContract.GetCustomerWithdrawals` | `/profile/withdrawal-requests` | لیست درخواست‌های برداشت | -| 4.9 | `WalletService.GetWithdrawalSettingsAsync` | `UserWalletContract.GetCustomerWithdrawalSettings` | `/profile/withdrawal-requests` | حداقل مبلغ برداشت | -| 4.10 | `WalletService.RequestWithdrawalAsync` | `UserWalletContract.CustomerWithdrawBalance` | `/profile/withdrawal-requests` | ثبت درخواست برداشت پاداش (با payoutId کمیسیون) | -| 4.11 | `CommissionService.GetWithdrawablePayoutsAsync` | `CommissionContract.GetMyCommissionPayouts` | `/profile/withdrawal-requests` | پاداش‌های قابل برداشت برای انتخاب | +| 4.8 | `WalletService.InitiateCreditChargeAsync` | `UserWalletContract.InitiateCreditCharge` | `/profile/charge-credit-wallet` | شروع شارژ کیف پول اصلی → درگاه | +| 4.9 | `WalletService.VerifyCreditChargeAsync` | `UserWalletContract.VerifyCreditCharge` | `/profile/payment-callback?type=credit-wallet` | تأیید شارژ کیف اصلی | +| 4.10 | `WalletService.GetWithdrawalsAsync` | `UserWalletContract.GetCustomerWithdrawals` | `/profile/withdrawal-requests` | لیست درخواست‌های برداشت | +| 4.11 | `WalletService.GetWithdrawalSettingsAsync` | `UserWalletContract.GetCustomerWithdrawalSettings` | `/profile/withdrawal-requests` | حداقل مبلغ برداشت | +| 4.12 | `WalletService.RequestWithdrawalAsync` | `UserWalletContract.CustomerWithdrawBalance` | `/profile/withdrawal-requests` | ثبت درخواست برداشت پاداش (با payoutId کمیسیون) | +| 4.13 | `CommissionService.GetWithdrawablePayoutsAsync` | `CommissionContract.GetMyCommissionPayouts` | `/profile/withdrawal-requests` | پاداش‌های قابل برداشت برای انتخاب | --- @@ -81,8 +83,9 @@ | 5.1 | `PackageContract.CustomerVerifyPackagePurchase` | `PackageContract` | `/profile/payment-callback` (type=package) | تأیید خرید پکیج | | 5.2 | `WalletService.VerifyMagicChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=magic-wallet) | تأیید شارژ Magic | | 5.3 | `WalletService.VerifyDiscountChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=discount-wallet) | تأیید شارژ تخفیفی | -| 5.4 | `DiscountOrderService.VerifyDiscountOrderPaymentAsync` | `DiscountOrderContract.CustomerVerifyDiscountOrderPayment` | `/profile/payment-callback` (type=discount-order) | تأیید پرداخت سفارش فروشگاه تخفیفی | -| 5.5 | `UserContract.GetUserForCustomer` + `AuthService.InitUserAuthInfo` | `UserContract` | `/profile/payment-callback` | به‌روزرسانی claims کاربر پس از پرداخت موفق | +| 5.4 | `WalletService.VerifyCreditChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=credit-wallet) | تأیید شارژ کیف اصلی | +| 5.5 | `DiscountOrderService.VerifyDiscountOrderPaymentAsync` | `DiscountOrderContract.CustomerVerifyDiscountOrderPayment` | `/profile/payment-callback` (type=discount-order) | تأیید پرداخت سفارش فروشگاه تخفیفی | +| 5.6 | `UserContract.GetUserForCustomer` + `AuthService.InitUserAuthInfo` | `UserContract` | `/profile/payment-callback` | به‌روزرسانی claims کاربر پس از پرداخت موفق | --- diff --git a/src/FrontOffice.Main/FrontOffice.Main.csproj b/src/FrontOffice.Main/FrontOffice.Main.csproj index a2e702d..ca06319 100644 --- a/src/FrontOffice.Main/FrontOffice.Main.csproj +++ b/src/FrontOffice.Main/FrontOffice.Main.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor new file mode 100644 index 0000000..611ecab --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor @@ -0,0 +1,126 @@ +@attribute [Route(RouteConstants.Profile.ChargeCreditWallet)] +@attribute [Authorize] + +شارژ کیف‌پول اصلی + + + + + + @if (_paymentResult != null) + { + + @if (_paymentResult == "success") + { + + شارژ کیف پول اصلی با موفقیت انجام شد! ✅ + + ادامه خرید + + + } + else if (_paymentResult == "cancelled") + { + پرداخت توسط شما لغو شد. + } + else + { + پرداخت ناموفق بود. لطفاً دوباره تلاش کنید. + } + + } + + + + + شارژ موجودی اصلی + + + + مبلغ وارد شده (به تومان) از طریق درگاه پرداخت به موجودی اصلی شما اضافه می‌شود. + از موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش استفاده می‌شود. + + + + + + + @foreach (var preset in _presetAmounts) + { + + @FormatToman(preset) + + } + + + @if (_chargeAmount > 0) + { + + + مبلغ پرداخت: + @FormatToman(_chargeAmount) + + + موجودی دریافتی: + @FormatToman(_chargeAmount) + + + } + + + @if (_isProcessing) + { + + در حال انتقال به درگاه... + } + else + { + پرداخت با کارت بانکی + } + + + + + + + + + موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش قابل استفاده است + + + حداقل مبلغ شارژ: ۱۰,۰۰۰ تومان + + + مبلغ واریزی بدون ضریب به موجودی اضافه می‌شود (۱:۱) + + + پرداخت از طریق درگاه بانکی انجام می‌شود + + + + + + diff --git a/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs new file mode 100644 index 0000000..4237af7 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs @@ -0,0 +1,98 @@ +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using MudBlazor; + +namespace FrontOffice.Main.Pages.Profile; + +public partial class ChargeCreditWallet : ComponentBase +{ + private bool _isProcessing; + private long _chargeAmount; + private string? _paymentResult; + private string? _pendingReturnUrl; + + private readonly long[] _presetAmounts = { 500_000, 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000 }; + + [SupplyParameterFromQuery(Name = "payment")] + public string? PaymentQueryParam { get; set; } + + [SupplyParameterFromQuery(Name = "amount")] + public long? AmountQueryParam { get; set; } + + [SupplyParameterFromQuery(Name = "returnUrl")] + public string? ReturnUrlQueryParam { get; set; } + + protected override void OnInitialized() + { + _paymentResult = PaymentQueryParam; + + if (AmountQueryParam is > 0) + _chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(AmountQueryParam.Value); + + if (CreditChargeNavigation.IsValidReturnUrl(ReturnUrlQueryParam)) + _pendingReturnUrl = ReturnUrlQueryParam; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && !string.IsNullOrEmpty(_pendingReturnUrl)) + { + await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl); + } + + await base.OnAfterRenderAsync(firstRender); + } + + private async Task StartCreditCharge() + { + if (_chargeAmount <= 0 || _isProcessing) return; + + _isProcessing = true; + StateHasChanged(); + + try + { + if (!string.IsNullOrEmpty(_pendingReturnUrl)) + await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl); + else + { + var storedReturnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime); + if (CreditChargeNavigation.IsValidReturnUrl(storedReturnUrl)) + _pendingReturnUrl = storedReturnUrl; + } + + var (success, gatewayUrl, error) = await WalletService.InitiateCreditChargeAsync(_chargeAmount); + + if (success && !string.IsNullOrEmpty(gatewayUrl)) + { + Navigation.NavigateTo(gatewayUrl, forceLoad: true); + } + else + { + Snackbar.Add(error ?? "خطا در ایجاد درخواست پرداخت", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"خطا: {ex.Message}", Severity.Error); + } + finally + { + _isProcessing = false; + StateHasChanged(); + } + } + + private async Task ContinueShoppingAsync() + { + var returnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime); + await CreditChargeNavigation.ClearReturnUrlAsync(JSRuntime); + + if (CreditChargeNavigation.IsValidReturnUrl(returnUrl)) + Navigation.NavigateTo(returnUrl!); + } + + private static string FormatToman(long toman) + => string.Format("{0:N0} تومان", toman); +} diff --git a/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor b/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor index a7639f5..08c1794 100644 --- a/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor +++ b/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor @@ -143,6 +143,9 @@ case "discount-wallet": await VerifyDiscountWalletCharge(); break; + case "credit-wallet": + await VerifyCreditWalletCharge(); + break; case "discount-order": await VerifyDiscountOrderPayment(); break; @@ -254,6 +257,35 @@ await UpdateWalletBalance(); } } + + /// + /// تأیید شارژ کیف پول اصلی + /// + private async Task VerifyCreditWalletCharge() + { + _successReturnUrl = "/profile/charge-credit-wallet?payment=success"; + _successReturnText = "بازگشت به شارژ کیف پول اصلی"; + _failReturnUrl = "/profile/charge-credit-wallet?payment=failed"; + _failReturnText = "بازگشت به شارژ کیف پول اصلی"; + + if (string.IsNullOrEmpty(Authority)) + { + _isSuccess = false; + _message = "کد Authority نامعتبر است"; + return; + } + + var (success, message) = await WalletService.VerifyCreditChargeAsync( + Authority, Status ?? "NOK"); + + _isSuccess = success; + _message = message; + + if (_isSuccess) + { + await UpdateWalletBalance(); + } + } /// /// تأیید پرداخت سفارش فروشگاه تخفیفی @@ -308,6 +340,7 @@ { "magic-wallet" => "/profile/magic-wallet", "discount-wallet" => "/profile/charge-discount-wallet", + "credit-wallet" => "/profile/charge-credit-wallet", "discount-order" => "/discount-store", _ => "/profile" }; diff --git a/src/FrontOffice.Main/Pages/Profile/Wallet.razor b/src/FrontOffice.Main/Pages/Profile/Wallet.razor index 219c273..6670c39 100644 --- a/src/FrontOffice.Main/Pages/Profile/Wallet.razor +++ b/src/FrontOffice.Main/Pages/Profile/Wallet.razor @@ -38,6 +38,17 @@ درخواست‌های برداشت + + + شارژ کیف‌پول اصلی + + TryHandleInsufficientBalanceAsync(long totalRequired) + { + if (walletBalance >= totalRequired) + return false; + + var shortfall = totalRequired - walletBalance; + var parameters = new DialogParameters + { + { x => x.CurrentBalance, walletBalance }, + { x => x.RequiredAmount, totalRequired }, + { x => x.ShortfallAmount, shortfall } + }; + + var dialog = await DialogService.ShowAsync( + "موجودی کافی نیست", + parameters, + new DialogOptions + { + CloseOnEscapeKey = true, + MaxWidth = MaxWidth.Small, + FullWidth = true + }); + + var result = await dialog.Result; + if (result is { Canceled: false } && result.Data is long chargeShortfall) + { + var chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(chargeShortfall); + Navigation.NavigateTo(CreditChargeNavigation.BuildChargeUrl( + chargeAmount, + RouteConstants.Store.CheckoutSummary)); + } + + return true; + } + private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); - /// - /// محاسبه مالیات بر ارزش افزوده - /// private long CalculateVAT() => VAT.CalculateVAT(Cart.Total); private static string GetProductImageUrl(string? imageUrl) => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/'); -} \ No newline at end of file +} diff --git a/src/FrontOffice.Main/Shared/InsufficientCreditDialog.razor b/src/FrontOffice.Main/Shared/InsufficientCreditDialog.razor new file mode 100644 index 0000000..da14999 --- /dev/null +++ b/src/FrontOffice.Main/Shared/InsufficientCreditDialog.razor @@ -0,0 +1,61 @@ + + + + + موجودی کیف پول اصلی کافی نیست + + + + + + برای تکمیل این سفارش، موجودی اصلی شما کافی نیست. می‌توانید کیف پول را شارژ کنید و سپس سفارش را ثبت کنید. + + + + + موجودی فعلی: + @FormatPrice(CurrentBalance) + + + مبلغ سفارش: + @FormatPrice(RequiredAmount) + + + + کمبود: + @FormatPrice(ShortfallAmount) + + + + + + + بستن + + شارژ حساب اصلی + + + + +@code { + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public long CurrentBalance { get; set; } + + [Parameter] + public long RequiredAmount { get; set; } + + [Parameter] + public long ShortfallAmount { get; set; } + + private void Cancel() => MudDialog.Cancel(); + + private void GoToCharge() => MudDialog.Close(DialogResult.Ok(ShortfallAmount)); + + private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); +} diff --git a/src/FrontOffice.Main/Utilities/CreditChargeNavigation.cs b/src/FrontOffice.Main/Utilities/CreditChargeNavigation.cs new file mode 100644 index 0000000..0cc0bbf --- /dev/null +++ b/src/FrontOffice.Main/Utilities/CreditChargeNavigation.cs @@ -0,0 +1,50 @@ +using Grpc.Core; +using Microsoft.JSInterop; + +namespace FrontOffice.Main.Utilities; + +public static class CreditChargeNavigation +{ + public const long MinChargeAmount = 10_000; + public const string ReturnUrlStorageKey = "credit_charge_return_url"; + private const string InsufficientBalanceMarker = "موجودی کیف پول کافی نیست"; + + public static long NormalizeChargeAmount(long shortfall) => + Math.Max(shortfall, MinChargeAmount); + + public static string BuildChargeUrl(long amount, string? returnUrl) + { + var url = $"{RouteConstants.Profile.ChargeCreditWallet}?amount={amount}"; + if (IsValidReturnUrl(returnUrl)) + url += $"&returnUrl={Uri.EscapeDataString(returnUrl!)}"; + return url; + } + + public static bool IsValidReturnUrl(string? url) => + !string.IsNullOrWhiteSpace(url) + && url.StartsWith('/') + && !url.StartsWith("//", StringComparison.Ordinal); + + public static bool IsInsufficientWalletBalanceMessage(string? message) => + message?.Contains(InsufficientBalanceMarker, StringComparison.Ordinal) == true; + + public static bool IsInsufficientWalletBalance(Exception ex) => + ex switch + { + RpcException rpc => IsInsufficientWalletBalanceMessage(rpc.Status.Detail) + || IsInsufficientWalletBalanceMessage(rpc.Message), + _ => IsInsufficientWalletBalanceMessage(ex.Message) + }; + + public static async Task SaveReturnUrlAsync(IJSRuntime js, string returnUrl) + { + if (!IsValidReturnUrl(returnUrl)) return; + await js.InvokeVoidAsync("sessionStorage.setItem", ReturnUrlStorageKey, returnUrl); + } + + public static async Task GetReturnUrlAsync(IJSRuntime js) => + await js.InvokeAsync("sessionStorage.getItem", ReturnUrlStorageKey); + + public static async Task ClearReturnUrlAsync(IJSRuntime js) => + await js.InvokeVoidAsync("sessionStorage.removeItem", ReturnUrlStorageKey); +} diff --git a/src/FrontOffice.Main/Utilities/RouteConstants.cs b/src/FrontOffice.Main/Utilities/RouteConstants.cs index 0c57869..7000f3f 100644 --- a/src/FrontOffice.Main/Utilities/RouteConstants.cs +++ b/src/FrontOffice.Main/Utilities/RouteConstants.cs @@ -24,6 +24,7 @@ public static class RouteConstants public const string Wallet = "/profile/wallet"; public const string MagicWallet = "/profile/magic-wallet"; public const string ChargeDiscountWallet = "/profile/charge-discount-wallet"; + public const string ChargeCreditWallet = "/profile/charge-credit-wallet"; public const string WithdrawalRequests = "/profile/withdrawal-requests"; } diff --git a/src/FrontOffice.Main/Utilities/WalletService.cs b/src/FrontOffice.Main/Utilities/WalletService.cs index 34e9d6a..0322f30 100644 --- a/src/FrontOffice.Main/Utilities/WalletService.cs +++ b/src/FrontOffice.Main/Utilities/WalletService.cs @@ -266,6 +266,26 @@ public class WalletService } } + public async Task<(bool Success, string? GatewayUrl, string? Error)> InitiateCreditChargeAsync(long amount) + { + try + { + var response = await _client.InitiateCreditChargeAsync(new InitiateCreditChargeRequest + { + Amount = amount + }); + + if (response.IsSuccess) + return (true, response.GatewayUrl, null); + + return (false, null, response.ErrorMessage); + } + catch (Grpc.Core.RpcException ex) + { + return (false, null, ex.Status.Detail ?? "خطا در ارتباط با سرور"); + } + } + // ============= Wallet Verify Methods ============= public async Task<(bool Success, string Message)> VerifyMagicChargeAsync(string authority, string status) @@ -304,6 +324,24 @@ public class WalletService } } + public async Task<(bool Success, string Message)> VerifyCreditChargeAsync(string authority, string status) + { + try + { + var response = await _client.VerifyCreditChargeAsync(new VerifyWalletChargeRequest + { + Authority = authority, + Status = status + }); + + return (response.Success, response.Message); + } + catch (Grpc.Core.RpcException ex) + { + return (false, ex.Status.Detail ?? "خطا در بررسی وضعیت پرداخت"); + } + } + public int GetWalletMode() { try