Compare commits

...

20 Commits

Author SHA1 Message Date
masoodafar-web 1914f703db Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 2m40s
2026-02-28 04:47:34 +03:30
masoodafar-web ff3696cda3 feat: implement payment verification methods for magic wallet and discount wallet
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m37s
2026-02-28 03:27:58 +03:30
masoodafar-web 2b1dc47dc7 cleanup: remove CallbackUrl from package purchase requests — CMS reads from config now
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m7s
2026-02-27 22:21:02 +03:30
masoodafar-web 2f9ef15859 fix: remove double ×10 conversion in wallet charge — FO should send Toman, not Rial
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m40s
Root cause: FrontOffice was converting Toman→Rial (×10) before sending to CMS,
but CMS ZarinPalService already does Toman→Rial conversion internally.
This caused double ×10 = amounts shown 10x higher in payment gateway.

Fixes:
- MagicWallet.razor.cs: remove ×10 conversion, send _chargeAmount (Toman) directly
- MagicWallet.razor: fix Max field (no /10), fix preset filter (no ×10)
- MagicWallet.razor.cs: fix FormatPrice — values from CMS are Toman, not Rial
- ChargeDiscountWallet.razor.cs: same fix — send Toman directly
- ClubMembershipContractDialog.razor: fix Price display (no /10)
2026-02-27 21:53:01 +03:30
masoodafar-web 5ded91a58a fix: payment success page — show TransactionId, real wallet balance, fix back button
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m53s
- کد ارجاعی: از TransactionId جدول تراکنش‌ها بجای RefId زرین‌پال
- موجودی: از WalletService.GetBalancesAsync() موجودی واقعی کیف پول اصلی
- حذف نمایش موجودی کیف پول اعتباری (نیازی نیست)
- دکمه بازگشت: Href=/profile بجای history.back() (که به درگاه می‌رفت)
2026-02-27 21:31:45 +03:30
masoodafar-web 71b7e40994 fix: revert Price/10 — DB stores Toman, display directly
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m37s
Previous fix incorrectly divided by 10 assuming DB was Rial.
DB values are in Toman, so FormattedPrice shows Price directly.
2026-02-27 21:02:54 +03:30
masoodafar-web 3c1a8ffd53 fix: 4 UI fixes for package display
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m54s
1. Toman/Rial mismatch — Price from server is in Rial, now divides by 10 for correct Toman display
2. ضریب تخفیف → ضریب اعتبار label change in dialog + packages page
3. Back button already functional (ArrowForward in TitleContent) — verified
4. HTML rendering in package description via MarkupString
2026-02-27 20:44:18 +03:30
masoodafar-web a3681a8bcd feat: داشبورد — لیست کاشی پکیج‌ها + مدال انتخاب روش پرداخت
- حذف متن hardcoded 'پکیج پایه ۵۶ میلیون تومان' و دیالوگ inline
- ایجاد PackagePurchaseDialog — کامپوننت مستقل 2 مرحله‌ای:
  مرحله ۱: نمایش کاشی‌های پکیج (ریسپانسیو ۲-۳ تایی)
  مرحله ۲: انتخاب روش پرداخت (درگاه بانکی / دایا الماس)
- دایا الماس فقط برای پکیج پایه + دور اول نمایش داده میشه
- پکیج‌ها داینامیک از API بارگذاری میشن
- تغییر متن CTA از 'شروع فرآیند تامین اعتبار' به 'تهیه پکیج'
- تغییر متن راهنما از 'پکیج پایه را تهیه کنید' به 'یکی از پکیج‌ها را تهیه کنید'
- CSS: hover effects + badge + responsive tiles
2026-02-27 20:14:40 +03:30
masoodafar-web ecc4f447ba F6+F7: مقادیر داینامیک کیف‌پول جادویی، اصلاح rename، افزایش سقف تخفیفی
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m34s
F6: MagicWallet.razor — جایگزینی ×2.5 و سقف‌های ثابت با مقادیر از API
    - WalletService: MagicMultiplier + MagicMaxCredit از gRPC response
    - Index.razor.cs: 'شارژ ×2.5' → 'شارژ چند‌برابری' (تایل داشبورد)
F7: ChargeDiscountWallet.razor — Max از 100M تومان به 1B تومان
اصلاح rename:
    - ConfigureServices: UserWalletChangeLog → UserWalletHistory
    - WalletService: اصلاح تمام رفرنس‌ها
NuGet: v0.0.189
2026-02-27 09:01:12 +03:30
masoodafar-web 474d364a25 feat(Q28): UI Guidance alerts across FrontOffice (G1-G7)
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m53s
Q28: Added informational/warning alerts to 7 FO pages:
G1: Packages — package system explanation
G2: Checkout — package benefits summary
G3: MyPackages — cycle completion guidance
G4: MagicWallet — per-package settings warning
G5: Commission — independent pool explanation
G6: Membership — single-sign contract info
G7: ActivationSection — estimated cost note
2026-02-27 05:40:04 +03:30
masoodafar-web 816dcb70d1 fix: remove hardcoded 56M amounts — dynamic package pricing
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m52s
- ActivationSection: load base package price via PackageService on init
- ClubMembershipContractDialog: dynamic package name/price in contract text
2026-02-27 02:41:24 +03:30
masoodafar-web 3bffc13ffd feat(T4.2+T4.3+F3): conditional payment, re-purchase, PV display
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m33s
T4.2: Conditional Payment in Checkout
- Load package flags via PackageService.GetAllPackagesAsync
- Respect SupportsDirectPurchase/SupportsDayaPurchase flags
- Add Daya loan payment button when supported
- Show alert when no payment method available

T4.3: MyPackages Re-Purchase Logic
- Load MagicWallet status to detect cycle completion
- Show re-purchase CTA when magic cycle is complete
- Show magic wallet progress bar during active cycle
- Display deposit progress, remaining, and credited amounts

F3: PV Display in Order Details
- Add CalculateOrderPVAsync to OrderService
- Show per-product PV and total PV in Store OrderDetail
- Update NuGet to v0.0.188
2026-02-26 22:01:55 +03:30
masoodafar-web a956cb90a1 feat(commission): add per-package filtering to commission pages
- CommissionDtos: add PackageId + PackageTitle to CommissionPayoutDto and WeeklyBalanceDto
- CommissionService: add packageId parameter to GetMyCommissionPayoutsAsync and GetMyWeeklyBalanceAsync
- CommissionDashboardPage: add package filter dropdown + package column in desktop/mobile views
- WeeklyBalancePage: add package filter dropdown + package chip in balance info section
- Update NuGet Foursat.CMSMicroservice.Protobuf to v0.0.187
2026-02-26 21:08:30 +03:30
masoodafar-web 40882c8b80 Phase 8d: NuGet bump 0.0.185 → 0.0.186 (proto cleanup) 2026-02-26 18:00:47 +03:30
masoodafar-web d71d463668 Phase 8c: FO Package pages — Customer RPCs + dynamic features
- NuGet: 0.0.182 → 0.0.185 + local feed source
- PackageDetail: GetPackageAsync → GetCustomerPackageDetailsAsync
  Features & specs now driven by API (PackageFeature) not hardcoded
- Packages.razor: Un-excluded from build, dynamic feature bullets
  from CustomerPackageModel (DiscountMultiplier, MagicWallet, etc.)
- PackageService: Enriched PackageDto with 8 new package fields
  GetUserPackageStatusAsync connected to real RPC (was stub)
2026-02-26 17:52:37 +03:30
masoodafar-web 0bbc11e82c Phase 8a: Wire up Checkout page to Customer* RPCs
- Checkout.razor.cs: Rewrite ProcessPayment() to use
  CustomerPurchasePackageAsync instead of disabled dead code
  (old TransactionsContract + UserOrderContract flow removed)
- Callback URL now points to /profile/payment-callback
  (reuses existing verify page with CustomerVerifyPackagePurchase)
- Remove unused DI: UserOrderContract, TransactionContract
- Remove unused usings: Transactions, UserOrder, WellKnownTypes
- Profile/Index.razor.cs: Rename basePackage → selectedPackage,
  tempCallbackUrl → callbackUrl (cosmetic)
- Build: 0 errors
2026-02-26 03:33:58 +03:30
masoodafar-web 71f391ac80 Phase 7b: Migrate deprecated RPCs to Customer* RPCs
- Profile/Index: InitiateBasePackagePayment → CustomerPurchasePackage
  - Fetches base package via GetCustomerPackages, passes PackageId + PurchaseMethod
- Profile/PaymentCallback: VerifyBasePackagePayment → CustomerVerifyPackagePurchase
  - Uses Authority/Status (from ZarinPal callback) + orderId (from embedded URL param)
  - TransactionId no longer required (CMS finds it internally)
2026-02-26 02:35:59 +03:30
masoodafar-web b82cac4af1 Cleanup: Remove hardcoded 'پکیج طلایی' labels, add PackageTitle to DTO
- MyPackages.razor: dynamic PackageTitle from DTO instead of hardcoded 'پکیج طلایی'
- Packages.razor: generic text for purchase status and info section
- PackageService.cs: add PackageTitle to UserPackageStatusDto record
2026-02-26 02:01:54 +03:30
masoodafar-web ab8693b381 fix: rename wallet labels — اصلی، اعتباری، پاداش تیمی
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m11s
- عادی → اصلی (کیف پول اصلی)
- شبکه → پاداش تیمی
- پورسانت → پاداش تیمی
- موجودی کیف پول → موجودی کیف پول اصلی

Files: Wallet.razor, WalletService.cs, MagicWallet.razor,
       Profile/Index.razor, PaymentCallback.razor
2026-02-23 23:24:29 +03:30
masoodafar-web 86f43d8fdd chore: prevent appsettings.Production.json from merging — keep ours strategy
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m0s
2026-02-23 00:02:31 +03:30
34 changed files with 1067 additions and 321 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ using CMSMicroservice.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.UserCarts; using CMSMicroservice.Protobuf.Protos.UserCarts;
using CMSMicroservice.Protobuf.Protos.UserOrder; using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserWallet; using CMSMicroservice.Protobuf.Protos.UserWallet;
using CMSMicroservice.Protobuf.Protos.UserWalletChangeLog; using CMSMicroservice.Protobuf.Protos.UserWalletHistory;
using CMSMicroservice.Protobuf.Protos.UserAddress; using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.Configuration; using CMSMicroservice.Protobuf.Protos.Configuration;
using CMSMicroservice.Protobuf.Protos.NetworkMembership; using CMSMicroservice.Protobuf.Protos.NetworkMembership;
@@ -128,7 +128,7 @@ public static class ConfigureServices
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserCarts.UserCartsContract.UserCartsContractClient>); services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserCarts.UserCartsContract.UserCartsContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.City.CityContract.CityContractClient>); services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.City.CityContract.CityContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserAddress.UserAddressContract.UserAddressContractClient>); services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserAddress.UserAddressContract.UserAddressContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserWalletChangeLog.UserWalletChangeLogContract.UserWalletChangeLogContractClient>); services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserWalletHistory.UserWalletHistoryContract.UserWalletHistoryContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.ClubMembership.ClubMembershipContract.ClubMembershipContractClient>); services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.ClubMembership.ClubMembershipContract.ClubMembershipContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.OtpToken.OtpTokenContract.OtpTokenContractClient>); services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.OtpToken.OtpTokenContract.OtpTokenContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Configuration.ConfigurationContract.ConfigurationContractClient>); services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Configuration.ConfigurationContract.ConfigurationContractClient>);
+1 -7
View File
@@ -11,7 +11,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" /> <PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
<!-- Replace all FrontOffice.BFF protobuf packages with CMS protobuf --> <!-- Replace all FrontOffice.BFF protobuf packages with CMS protobuf -->
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.182" /> <PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.190" />
<!-- <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />--> <!-- <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />-->
<PackageReference Include="MudBlazor" Version="8.14.0" /> <PackageReference Include="MudBlazor" Version="8.14.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" /> <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
@@ -56,12 +56,6 @@
<Content Include="..\.dockerignore"> <Content Include="..\.dockerignore">
<Link>.dockerignore</Link> <Link>.dockerignore</Link>
</Content> </Content>
<Content Remove="Pages\Package\Packages.razor" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Pages\Package\Packages.razor.cs" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+2
View File
@@ -2,6 +2,8 @@
<configuration> <configuration>
<packageSources> <packageSources>
<clear /> <clear />
<!-- Local packages (dev builds) -->
<add key="Local" value="/home/masoud/Apps/project/FourSat/nupkg" />
<!-- Nexus (hosts FourSat packages + proxies nuget.org) --> <!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" /> <add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
</packageSources> </packageSources>
+27 -1
View File
@@ -27,6 +27,11 @@
<MudPaper Elevation="2" Class="pa-4 rounded-lg"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h5" Class="mb-4">جزئیات پکیج</MudText> <MudText Typo="Typo.h5" Class="mb-4">جزئیات پکیج</MudText>
@* ── G2: توضیح خرید پکیج (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
با خرید این پکیج، سقف پاداش هفتگی، ضریب کیف‌پول جادویی و دسترسی به فیچرهای اختصاصی برای شما فعال می‌شود.
</MudAlert>
@if (_selectedPackage != null) @if (_selectedPackage != null)
{ {
<MudCard> <MudCard>
@@ -207,11 +212,32 @@
FullWidth="true" FullWidth="true"
StartIcon="@Icons.Material.Filled.Payment" StartIcon="@Icons.Material.Filled.Payment"
OnClick="ProcessPayment" OnClick="ProcessPayment"
Disabled="@(!CanProceedToPayment || _isProcessingPayment)" Disabled="@(!CanProceedToPayment || _isProcessingPayment || !_selectedPackage.SupportsDirectPurchase)"
Class="mt-2"> Class="mt-2">
@(_isProcessingPayment ? "در حال پردازش..." : "پرداخت آنلاین") @(_isProcessingPayment ? "در حال پردازش..." : "پرداخت آنلاین")
</MudButton> </MudButton>
@if (_selectedPackage.SupportsDayaPurchase)
{
<MudButton Variant="Variant.Filled"
Color="Color.Tertiary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.Diamond"
OnClick="DayaLoanPayment"
Disabled="@(!CanProceedToPayment)"
Class="mt-2">
تأمین اعتبار الماسی دایا
</MudButton>
}
@if (!_selectedPackage.SupportsDirectPurchase && !_selectedPackage.SupportsDayaPurchase)
{
<MudAlert Severity="Severity.Warning" Dense="true" Class="mt-2">
این پکیج در حال حاضر قابل خرید نیست.
</MudAlert>
}
@if (!CanProceedToPayment) @if (!CanProceedToPayment)
{ {
<MudText Typo="Typo.caption" Color="Color.Error" Align="Align.Center"> <MudText Typo="Typo.caption" Color="Color.Error" Align="Align.Center">
+41 -39
View File
@@ -1,11 +1,9 @@
using CMSMicroservice.Protobuf.Protos.Package; using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.Transactions;
using CMSMicroservice.Protobuf.Protos.UserAddress; using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using FrontOffice.Main.Utilities; using FrontOffice.Main.Utilities;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor; using MudBlazor;
using Severity = MudBlazor.Severity; using Severity = MudBlazor.Severity;
@@ -13,10 +11,9 @@ namespace FrontOffice.Main.Pages;
public partial class Checkout public partial class Checkout
{ {
[Inject] private PackageService PackageService { get; set; } = default!;
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!; [Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!; [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
[Inject] private TransactionsContract.TransactionsContractClient TransactionContract { get; set; } = default!;
[Parameter] public long? PackageId { get; set; } [Parameter] public long? PackageId { get; set; }
@@ -52,15 +49,18 @@ public partial class Checkout
{ {
try try
{ {
var response = await PackageClient.GetPackageAsync(new() { Id = PackageId.Value }); var packages = await PackageService.GetAllPackagesAsync();
if (response != null) var pkg = packages.FirstOrDefault(p => p.Id == PackageId.Value);
if (pkg != null)
{ {
_selectedPackage = new Pack( _selectedPackage = new Pack(
Id: response.Id, Id: pkg.Id,
Title: response.Title, Title: pkg.Title,
Body: response.Description, Body: pkg.Description,
Image: response.ImagePath ?? string.Empty, Image: pkg.ImageUrl,
Price: response.Price Price: pkg.Price,
SupportsDirectPurchase: pkg.SupportsDirectPurchase,
SupportsDayaPurchase: pkg.SupportsDayaPurchase
); );
_finalPrice = _selectedPackage.Price; _finalPrice = _selectedPackage.Price;
} }
@@ -173,9 +173,6 @@ public partial class Checkout
private async Task ProcessPayment() private async Task ProcessPayment()
{ {
Snackbar.Add("درگاه پرداخت متصل نیست! لطفا در زمان دیگری مجددا تلاش فرمایید!", Severity.Warning);
return;
if (!CanProceedToPayment || _selectedPackage == null || _selectedAddress == null) if (!CanProceedToPayment || _selectedPackage == null || _selectedAddress == null)
{ {
Snackbar.Add("لطفاً پکیج و آدرس را انتخاب کنید.", Severity.Warning); Snackbar.Add("لطفاً پکیج و آدرس را انتخاب کنید.", Severity.Warning);
@@ -186,33 +183,26 @@ public partial class Checkout
try try
{ {
// Step 1: Create payment request var response = await PackageClient.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
var paymentRequest = new CustomerPaymentRequestRequest
{ {
Amount = _finalPrice,
CallbackUrl = $"{Navigation.BaseUri}checkout/callback",
Description = $"خرید پکیج {_selectedPackage.Title}",
Currency = CurrencyEnum.Irt,
Type = TransactionTypeEnum.Real
};
var paymentResponse = await TransactionContract.CustomerPaymentRequestAsync(paymentRequest);
if (string.IsNullOrEmpty(paymentResponse.PaymentGWUrl))
Snackbar.Add("آدرس درگاه پرداخت دریافت نشد.", Severity.Error);
// Step 2: Create user order
var orderRequest = new CreateNewUserOrderRequest
{
Amount = _finalPrice,
PackageId = _selectedPackage.Id, PackageId = _selectedPackage.Id,
PaymentStatus = CMSMicroservice.Protobuf.Protos.PaymentStatus.Pending PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
}; });
var orderResponse = await UserOrderContract.CreateNewUserOrderAsync(orderRequest); if (!response.Success)
{
Snackbar.Add(response.Message ?? "خطا در آغاز فرآیند پرداخت", Severity.Error);
return;
}
// Step 3: Redirect to payment gateway if (!string.IsNullOrEmpty(response.PaymentGatewayUrl))
Navigation.NavigateTo(paymentResponse.PaymentGWUrl); {
Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true);
}
else
{
Snackbar.Add("خطا در دریافت آدرس درگاه پرداخت", Severity.Error);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -225,5 +215,17 @@ public partial class Checkout
} }
} }
private record Pack(long Id, string Title, string Body, string Image, long Price); private async Task DayaLoanPayment()
{
if (_selectedAddress == null)
{
Snackbar.Add("لطفاً آدرس را انتخاب کنید.", Severity.Warning);
return;
}
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
}
private record Pack(long Id, string Title, string Body, string Image, long Price, bool SupportsDirectPurchase = true, bool SupportsDayaPurchase = false);
} }
@@ -4,6 +4,11 @@
<MudPaper Elevation="2" Class="pa-4 rounded-lg"> <MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">فعال‌سازی یا تمدید عضویت</MudText> <MudText Typo="Typo.h6" Class="mb-3">فعال‌سازی یا تمدید عضویت</MudText>
@* ── G7: راهنمای هزینه فعال‌سازی (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
هزینه تخمینی بر اساس پکیج انتخابی محاسبه می‌شود. هر پکیج قیمت و شرایط فعال‌سازی متفاوتی دارد.
</MudAlert>
<MudForm @ref="_form" @bind-IsValid="_formIsValid"> <MudForm @ref="_form" @bind-IsValid="_formIsValid">
<MudGrid Spacing="2"> <MudGrid Spacing="2">
<MudItem xs="12" sm="6"> <MudItem xs="12" sm="6">
@@ -10,6 +10,7 @@ public partial class ActivationSection : ComponentBase
[Inject] private ClubMembershipService ClubService { get; set; } = default!; [Inject] private ClubMembershipService ClubService { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!; [Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!; [Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
[Inject] private PackageService PackageService { get; set; } = default!;
[Parameter] public EventCallback OnActivationSuccess { get; set; } [Parameter] public EventCallback OnActivationSuccess { get; set; }
@@ -21,6 +22,18 @@ public partial class ActivationSection : ComponentBase
private long _packageId = 1; private long _packageId = 1;
private int _durationMonths = 1; private int _durationMonths = 1;
private string? _activationCode; private string? _activationCode;
private long _basePackagePrice;
protected override async Task OnInitializedAsync()
{
try
{
var packages = await PackageService.GetAllPackagesAsync();
var basePackage = packages.FirstOrDefault(p => p.IsBasePackage) ?? packages.FirstOrDefault();
_basePackagePrice = basePackage?.Price ?? 0;
}
catch { /* fallback to 0 */ }
}
private async Task HandleActivateAsync() private async Task HandleActivateAsync()
{ {
@@ -75,9 +88,8 @@ public partial class ActivationSection : ComponentBase
private string GetEstimatedCost() private string GetEstimatedCost()
{ {
// فرمول تقریبی: 56M per month (base amount from BFF implementation) if (_basePackagePrice <= 0) return "در حال بارگذاری...";
var baseAmount = 56_000_000; var total = _basePackagePrice * _durationMonths;
var total = baseAmount * _durationMonths;
return $"{total:N0} تومان"; return $"{total:N0} تومان";
} }
} }
@@ -9,6 +9,11 @@
<MudStack Spacing="3"> <MudStack Spacing="3">
<PageHeader Title="عضویت باشگاه مشتریان" BackHref="@RouteConstants.Profile.Index" /> <PageHeader Title="عضویت باشگاه مشتریان" BackHref="@RouteConstants.Profile.Index" />
@* ── G6: راهنمای قرارداد باشگاه (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Gavel">
قرارداد باشگاه مشتریان فقط یک‌بار امضا می‌شود و با هر بار خرید پکیج جدید نیازی به امضای مجدد نیست.
</MudAlert>
@if (_isLoading) @if (_isLoading)
{ {
<LoadingState Message="در حال دریافت اطلاعات عضویت..." /> <LoadingState Message="در حال دریافت اطلاعات عضویت..." />
@@ -10,10 +10,15 @@
<MudStack Spacing="3"> <MudStack Spacing="3">
<PageHeader Title="پاداش‌های من" BackHref="@RouteConstants.Profile.Index" /> <PageHeader Title="پاداش‌های من" BackHref="@RouteConstants.Profile.Index" />
@* ── G5: توضیح محاسبه پاداش (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Info">
پاداش هفتگی بر اساس هر پکیج جداگانه محاسبه می‌شود. هر پکیج Pool پورسانت مستقل دارد و Carryover بر اساس پکیج زیرمجموعه‌هاست.
</MudAlert>
<!-- فیلترها --> <!-- فیلترها -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg"> <MudPaper Elevation="2" Class="pa-3 rounded-lg">
<MudGrid Spacing="2"> <MudGrid Spacing="2">
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="3">
<WeekSelector @ref="_weekSelector" <WeekSelector @ref="_weekSelector"
@bind-Value="_selectedWeekDefinition" @bind-Value="_selectedWeekDefinition"
Label="انتخاب هفته" Label="انتخاب هفته"
@@ -23,7 +28,7 @@
Clearable="true" Clearable="true"
OnlyActive="false" /> OnlyActive="false" />
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="3">
<MudSelect T="string" @bind-Value="_filterStatus" Label="وضعیت" Variant="Variant.Outlined"> <MudSelect T="string" @bind-Value="_filterStatus" Label="وضعیت" Variant="Variant.Outlined">
<MudSelectItem T="string" Value="@string.Empty">همه</MudSelectItem> <MudSelectItem T="string" Value="@string.Empty">همه</MudSelectItem>
<MudSelectItem T="string" Value="@("Pending")">در انتظار</MudSelectItem> <MudSelectItem T="string" Value="@("Pending")">در انتظار</MudSelectItem>
@@ -34,7 +39,16 @@
<MudSelectItem T="string" Value="@("Cancelled")">لغو شده</MudSelectItem> <MudSelectItem T="string" Value="@("Cancelled")">لغو شده</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
<MudItem xs="12" sm="12" md="4"> <MudItem xs="12" sm="6" md="3">
<MudSelect T="long?" @bind-Value="_filterPackageId" Label="پکیج" Variant="Variant.Outlined" Clearable="true">
<MudSelectItem T="long?" Value="@((long?)null)">همه پکیج‌ها</MudSelectItem>
@foreach (var pkg in _packages)
{
<MudSelectItem T="long?" Value="@((long?)pkg.Id)">@pkg.Title</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudStack Row="true" Spacing="2" Style="height: 100%;" AlignItems="AlignItems.End"> <MudStack Row="true" Spacing="2" Style="height: 100%;" AlignItems="AlignItems.End">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ApplyFiltersAsync" StartIcon="@Icons.Material.Filled.FilterList" FullWidth="true"> <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ApplyFiltersAsync" StartIcon="@Icons.Material.Filled.FilterList" FullWidth="true">
اعمال فیلتر اعمال فیلتر
@@ -87,6 +101,7 @@
<MudTable Items="_payouts" Hover="true" Striped="true" Dense="true" FixedHeader="true" Height="400px"> <MudTable Items="_payouts" Hover="true" Striped="true" Dense="true" FixedHeader="true" Height="400px">
<HeaderContent> <HeaderContent>
<MudTh>هفته</MudTh> <MudTh>هفته</MudTh>
<MudTh>پکیج</MudTh>
<MudTh>امتیاز</MudTh> <MudTh>امتیاز</MudTh>
<MudTh>مبلغ (تومان)</MudTh> <MudTh>مبلغ (تومان)</MudTh>
<MudTh>وضعیت</MudTh> <MudTh>وضعیت</MudTh>
@@ -97,6 +112,11 @@
<MudTd DataLabel="هفته"> <MudTd DataLabel="هفته">
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@context.WeekDisplayName</MudChip> <MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@context.WeekDisplayName</MudChip>
</MudTd> </MudTd>
<MudTd DataLabel="پکیج">
<MudChip T="string" Color="Color.Tertiary" Size="Size.Small">
@(string.IsNullOrEmpty(context.PackageTitle) ? "-" : context.PackageTitle)
</MudChip>
</MudTd>
<MudTd DataLabel="امتیاز">@context.BalancesEarned</MudTd> <MudTd DataLabel="امتیاز">@context.BalancesEarned</MudTd>
<MudTd DataLabel="مبلغ"> <MudTd DataLabel="مبلغ">
<MudText Color="Color.Success"><strong>@context.AmountFormatted</strong></MudText> <MudText Color="Color.Success"><strong>@context.AmountFormatted</strong></MudText>
@@ -127,6 +147,10 @@
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@payout.WeekDisplayName</MudChip> <MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@payout.WeekDisplayName</MudChip>
<MudChip T="string" Color="@GetStatusColor(payout.StatusColor)" Size="Size.Small">@payout.StatusText</MudChip> <MudChip T="string" Color="@GetStatusColor(payout.StatusColor)" Size="Size.Small">@payout.StatusText</MudChip>
</MudStack> </MudStack>
@if (!string.IsNullOrEmpty(payout.PackageTitle))
{
<MudChip T="string" Color="Color.Tertiary" Size="Size.Small">@payout.PackageTitle</MudChip>
}
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Color="Color.Success">@payout.AmountFormatted</MudText> <MudText Typo="Typo.h6" Color="Color.Success">@payout.AmountFormatted</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">امتیاز: @payout.BalancesEarned</MudText> <MudText Typo="Typo.caption" Class="mud-text-secondary">امتیاز: @payout.BalancesEarned</MudText>
@@ -8,8 +8,10 @@ namespace FrontOffice.Main.Pages.Commission;
public partial class CommissionDashboardPage : ComponentBase public partial class CommissionDashboardPage : ComponentBase
{ {
[Inject] private CommissionService CommissionService { get; set; } = default!; [Inject] private CommissionService CommissionService { get; set; } = default!;
[Inject] private PackageService PackageService { get; set; } = default!;
private List<CommissionPayoutDto> _payouts = new(); private List<CommissionPayoutDto> _payouts = new();
private List<PackageDto> _packages = new();
private int _totalCount; private int _totalCount;
private long _totalAmount; private long _totalAmount;
private int _pageNumber = 1; private int _pageNumber = 1;
@@ -19,12 +21,26 @@ public partial class CommissionDashboardPage : ComponentBase
private WeekSelector? _weekSelector; private WeekSelector? _weekSelector;
private WeekDefinitionDto? _selectedWeekDefinition; private WeekDefinitionDto? _selectedWeekDefinition;
private string _filterStatus = string.Empty; private string _filterStatus = string.Empty;
private long? _filterPackageId;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await LoadPackagesAsync();
await LoadPayoutsAsync(); await LoadPayoutsAsync();
} }
private async Task LoadPackagesAsync()
{
try
{
_packages = await PackageService.GetAllPackagesAsync();
}
catch
{
_packages = new List<PackageDto>();
}
}
private async Task LoadPayoutsAsync() private async Task LoadPayoutsAsync()
{ {
try try
@@ -35,7 +51,8 @@ public partial class CommissionDashboardPage : ComponentBase
weekDefinitionId, weekDefinitionId,
_filterStatus, _filterStatus,
_pageNumber, _pageNumber,
_pageSize _pageSize,
_filterPackageId
); );
_payouts = result.Payouts; _payouts = result.Payouts;
_totalCount = result.TotalCount; _totalCount = result.TotalCount;
@@ -61,6 +78,7 @@ public partial class CommissionDashboardPage : ComponentBase
{ {
_selectedWeekDefinition = null; _selectedWeekDefinition = null;
_filterStatus = string.Empty; _filterStatus = string.Empty;
_filterPackageId = null;
_pageNumber = 1; _pageNumber = 1;
await LoadPayoutsAsync(); await LoadPayoutsAsync();
} }
@@ -12,7 +12,7 @@
<!-- انتخاب هفته --> <!-- انتخاب هفته -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg"> <MudPaper Elevation="2" Class="pa-3 rounded-lg">
<MudGrid Spacing="2"> <MudGrid Spacing="2">
<MudItem xs="8" sm="6" md="6"> <MudItem xs="8" sm="4" md="4">
<WeekSelector @ref="_weekSelector" <WeekSelector @ref="_weekSelector"
@bind-Value="_selectedWeekDefinition" @bind-Value="_selectedWeekDefinition"
Label="انتخاب هفته" Label="انتخاب هفته"
@@ -21,15 +21,15 @@
Dense="false" Dense="false"
OnlyActive="true" /> OnlyActive="true" />
</MudItem> </MudItem>
@* <MudItem xs="4" sm="3" md="3"> *@ <MudItem xs="4" sm="4" md="4">
@* <MudButton Variant="Variant.Outlined" *@ <MudSelect T="long?" @bind-Value="_filterPackageId" Label="پکیج" Variant="Variant.Outlined" Clearable="true">
@* OnClick="LoadCurrentWeekAsync" *@ <MudSelectItem T="long?" Value="@((long?)null)">همه پکیج‌ها</MudSelectItem>
@* StartIcon="@Icons.Material.Filled.Today" *@ @foreach (var pkg in _packages)
@* FullWidth="true" *@ {
@* Style="height: 56px;"> *@ <MudSelectItem T="long?" Value="@((long?)pkg.Id)">@pkg.Title</MudSelectItem>
@* هفته جاری *@ }
@* </MudButton> *@ </MudSelect>
@* </MudItem> *@ </MudItem>
<MudItem xs="4" sm="3" md="3" Class="pt-6"> <MudItem xs="4" sm="3" md="3" Class="pt-6">
<MudButton Variant="Variant.Filled" <MudButton Variant="Variant.Filled"
Color="Color.Primary" Color="Color.Primary"
@@ -66,6 +66,12 @@
<MudItem xs="6" sm="4"> <MudItem xs="6" sm="4">
<MudText Typo="Typo.caption" Class="mud-text-secondary">پایان: <strong>@_weeklyBalance.EndDatePersian</strong></MudText> <MudText Typo="Typo.caption" Class="mud-text-secondary">پایان: <strong>@_weeklyBalance.EndDatePersian</strong></MudText>
</MudItem> </MudItem>
@if (!string.IsNullOrEmpty(_weeklyBalance.PackageTitle))
{
<MudItem xs="12" sm="12">
<MudChip T="string" Color="Color.Info" Size="Size.Small" Icon="@Icons.Material.Filled.Inventory2">@_weeklyBalance.PackageTitle</MudChip>
</MudItem>
}
</MudGrid> </MudGrid>
</MudPaper> </MudPaper>
@@ -8,6 +8,7 @@ namespace FrontOffice.Main.Pages.Commission;
public partial class WeeklyBalancePage : ComponentBase public partial class WeeklyBalancePage : ComponentBase
{ {
[Inject] private CommissionService CommissionService { get; set; } = default!; [Inject] private CommissionService CommissionService { get; set; } = default!;
[Inject] private PackageService PackageService { get; set; } = default!;
[Inject] private ISnackbar SnackbarService { get; set; } = default!; [Inject] private ISnackbar SnackbarService { get; set; } = default!;
[Parameter] [Parameter]
@@ -17,6 +18,8 @@ public partial class WeeklyBalancePage : ComponentBase
private WeekSelector? _weekSelector; private WeekSelector? _weekSelector;
private WeekDefinitionDto? _selectedWeekDefinition; private WeekDefinitionDto? _selectedWeekDefinition;
private WeeklyBalanceDto? _weeklyBalance; private WeeklyBalanceDto? _weeklyBalance;
private List<PackageDto> _packages = new();
private long? _filterPackageId;
private bool _isLoading = true; private bool _isLoading = true;
private bool _hasError = false; private bool _hasError = false;
@@ -34,6 +37,8 @@ public partial class WeeklyBalancePage : ComponentBase
{ {
if (firstRender) if (firstRender)
{ {
await LoadPackagesAsync();
// Wait for WeekSelector to initialize and load its cached weeks // Wait for WeekSelector to initialize and load its cached weeks
if (_weekSelector != null) if (_weekSelector != null)
{ {
@@ -88,7 +93,7 @@ public partial class WeeklyBalancePage : ComponentBase
long? weekDefinitionId = _selectedWeekDefinition?.Id; long? weekDefinitionId = _selectedWeekDefinition?.Id;
_weeklyBalance = await CommissionService.GetMyWeeklyBalanceAsync(weekDefinitionId); _weeklyBalance = await CommissionService.GetMyWeeklyBalanceAsync(weekDefinitionId, _filterPackageId);
// Update week display name from selected definition if available // Update week display name from selected definition if available
if (_selectedWeekDefinition != null && _weeklyBalance != null) if (_selectedWeekDefinition != null && _weeklyBalance != null)
@@ -113,6 +118,18 @@ public partial class WeeklyBalancePage : ComponentBase
} }
} }
private async Task LoadPackagesAsync()
{
try
{
_packages = await PackageService.GetAllPackagesAsync();
}
catch
{
_packages = new();
}
}
private async Task LoadCurrentWeekAsync() private async Task LoadCurrentWeekAsync()
{ {
if (_weekSelector != null) if (_weekSelector != null)
@@ -20,7 +20,7 @@
<MudIcon Icon="@Icons.Material.Filled.CardGiftcard" Size="Size.Large" Color="Color.Primary" Style="font-size: 80px;" /> <MudIcon Icon="@Icons.Material.Filled.CardGiftcard" Size="Size.Large" Color="Color.Primary" Style="font-size: 80px;" />
<MudText Typo="Typo.h5">شما هنوز پکیجی خریداری نکرده‌اید</MudText> <MudText Typo="Typo.h5">شما هنوز پکیجی خریداری نکرده‌اید</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width: 500px;"> <MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width: 500px;">
با خرید پکیج طلایی، به باشگاه مشتریان بپیوندید و از مزایای ویژه مانند پاداش هفتگی و تیم‌سازی بهره‌مند شوید. با خرید پکیج، به باشگاه مشتریان بپیوندید و از مزایای ویژه مانند پاداش هفتگی و تیم‌سازی بهره‌مند شوید.
</MudText> </MudText>
<MudButton Variant="Variant.Filled" <MudButton Variant="Variant.Filled"
Color="Color.Primary" Color="Color.Primary"
@@ -34,7 +34,76 @@
} }
else else
{ {
@* ── G3: راهنمای پکیج‌های من (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-2" Icon="@Icons.Material.Filled.Info">
بعد از تکمیل چرخه کیف‌پول جادویی، می‌توانید مجدداً پکیج خریداری کرده و دور جدیدی را آغاز کنید.
</MudAlert>
<MudGrid Spacing="4"> <MudGrid Spacing="4">
@* Re-Purchase / Cycle Progress Section *@
@if (_canRepurchase)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Success" Icon="@Icons.Material.Filled.Celebration" Class="mb-0">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Style="width:100%">
<MudText Typo="Typo.body1">
🎉 چرخه جادویی تکمیل شد! می‌توانید پکیج جدید بخرید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ShoppingCart"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Package.List))">
خرید پکیج جدید
</MudButton>
</MudStack>
</MudAlert>
</MudItem>
}
else if (_magicStatus != null && _magicStatus.WalletMode == 1)
{
<MudItem xs="12">
<MudPaper Elevation="2" Class="pa-6">
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.AutoAwesome" Color="Color.Warning" />
<MudText Typo="Typo.h6">پیشرفت چرخه جادویی</MudText>
@if (_magicStatus.PurchaseCycleCount > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Info">دور @(_magicStatus.PurchaseCycleCount + 1)</MudChip>
}
</MudStack>
<MudDivider />
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">واریز انجام‌شده:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(_magicStatus.MagicTotalDeposited)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">سقف واریز:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(_magicStatus.MagicMaxDeposit)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">باقیمانده:</MudText>
<MudText Typo="Typo.body2" Color="Color.Warning">@FormatPrice(_magicStatus.MagicRemainingDeposit)</MudText>
</MudStack>
<MudProgressLinear Color="Color.Primary" Value="@GetDepositProgress()" Class="my-2" Rounded="true" Size="Size.Large">
<MudText Typo="Typo.caption"><b>@GetDepositProgress()%</b></MudText>
</MudProgressLinear>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">اعتبار دریافتی:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(_magicStatus.MagicTotalCredited)</MudText>
</MudStack>
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
}
<!-- Package Status Card --> <!-- Package Status Card -->
<MudItem xs="12" md="6"> <MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-6 h-100"> <MudPaper Elevation="2" Class="pa-6 h-100">
@@ -49,7 +118,7 @@
<MudStack Spacing="2"> <MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween"> <MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">نوع پکیج:</MudText> <MudText Typo="Typo.body2" Class="mud-text-secondary">نوع پکیج:</MudText>
<MudText Typo="Typo.body2">پکیج طلایی</MudText> <MudText Typo="Typo.body2">@_userStatus.PackageTitle</MudText>
</MudStack> </MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween"> <MudStack Row="true" Justify="Justify.SpaceBetween">
@@ -146,7 +215,7 @@
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Stars" Color="Color.Warning" /> <MudIcon Icon="@Icons.Material.Filled.Stars" Color="Color.Warning" />
<MudText Typo="Typo.h6">مزایای پکیج طلایی</MudText> <MudText Typo="Typo.h6">مزایای پکیج</MudText>
</MudStack> </MudStack>
<MudDivider /> <MudDivider />
@@ -9,7 +9,9 @@ public partial class MyPackages : ComponentBase
[Inject] private PackageService PackageService { get; set; } = default!; [Inject] private PackageService PackageService { get; set; } = default!;
private UserPackageStatusDto? _userStatus; private UserPackageStatusDto? _userStatus;
private MagicWalletStatus? _magicStatus;
private bool _isLoading = true; private bool _isLoading = true;
private bool _canRepurchase = false;
private List<BreadcrumbItem> _breadcrumbItems = new() private List<BreadcrumbItem> _breadcrumbItems = new()
{ {
@@ -30,6 +32,18 @@ public partial class MyPackages : ComponentBase
try try
{ {
_userStatus = await PackageService.GetUserPackageStatusAsync(); _userStatus = await PackageService.GetUserPackageStatusAsync();
if (_userStatus?.HasPurchasedPackage == true)
{
_magicStatus = await WalletService.GetMagicWalletStatusAsync();
// Cycle complete: wallet back to Normal mode, at least one cycle done,
// and deposit ceiling fully used (remaining = 0)
_canRepurchase = _magicStatus != null
&& _magicStatus.WalletMode == 0 // Normal mode
&& _magicStatus.PurchaseCycleCount >= 1 // At least one cycle completed
&& _magicStatus.MagicRemainingDeposit == 0; // Deposit ceiling used up
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -67,4 +81,11 @@ public partial class MyPackages : ComponentBase
{ {
return string.Format("{0:N0} تومان", price); return string.Format("{0:N0} تومان", price);
} }
private int GetDepositProgress()
{
if (_magicStatus == null || _magicStatus.MagicMaxDeposit <= 0) return 0;
var pct = (int)((_magicStatus.MagicTotalDeposited * 100) / _magicStatus.MagicMaxDeposit);
return Math.Min(pct, 100);
}
} }
@@ -5,6 +5,11 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-8"> <MudContainer MaxWidth="MaxWidth.Large" Class="py-8">
<PageHeader Title="پکیج‌های سرمایه‌گذاری" BackHref="@RouteConstants.Profile.Index" /> <PageHeader Title="پکیج‌های سرمایه‌گذاری" BackHref="@RouteConstants.Profile.Index" />
@* ── G1: راهنمای سیستم پکیج‌بیس (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-4" Icon="@Icons.Material.Filled.Info">
هر پکیج ویژگی‌ها و مزایای منحصربه‌فردی دارد. با انتخاب پکیج مناسب، از سقف پاداش و ضریب کیف‌پول جادویی متفاوتی بهره‌مند می‌شوید.
</MudAlert>
@if (_isLoading) @if (_isLoading)
{ {
<MudStack AlignItems="AlignItems.Center" Class="py-16"> <MudStack AlignItems="AlignItems.Center" Class="py-16">
@@ -27,7 +32,7 @@
{ {
<MudAlert Severity="Severity.Success" Class="mb-6" Icon="@Icons.Material.Filled.CheckCircle"> <MudAlert Severity="Severity.Success" Class="mb-6" Icon="@Icons.Material.Filled.CheckCircle">
<MudText> <MudText>
شما قبلاً پکیج طلایی را خریداری کرده‌اید. شما قبلاً پکیج را خریداری کرده‌اید.
@if (_userStatus.IsClubMemberActive) @if (_userStatus.IsClubMemberActive)
{ {
<span>عضویت باشگاه شما فعال است.</span> <span>عضویت باشگاه شما فعال است.</span>
@@ -64,20 +69,43 @@
<MudText Typo="Typo.h6" Color="Color.Success">@package.FormattedPrice</MudText> <MudText Typo="Typo.h6" Color="Color.Success">@package.FormattedPrice</MudText>
</MudStack> </MudStack>
<!-- Features Preview --> <!-- Package Highlights -->
<MudStack Spacing="1" Class="mt-2"> <MudStack Spacing="1" Class="mt-2">
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center"> @if (package.SupportsDirectPurchase)
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" /> {
<MudText Typo="Typo.caption">عضویت در باشگاه مشتریان</MudText> <MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
</MudStack> <MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center"> <MudText Typo="Typo.caption">پرداخت مستقیم</MudText>
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" /> </MudStack>
<MudText Typo="Typo.caption">دریافت پاداش هفتگی</MudText> }
</MudStack> @if (package.SupportsDayaPurchase)
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center"> {
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" /> <MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption">تیم‌سازی نامحدود</MudText> <MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
</MudStack> <MudText Typo="Typo.caption">پرداخت با اعتبار دایا</MudText>
</MudStack>
}
@if (package.DiscountMultiplier > 0)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">ضریب اعتبار: @package.DiscountMultiplier.ToString("F1")x</MudText>
</MudStack>
}
@if (package.MagicWalletMultiplier > 0)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">کیف پول جادویی: @package.MagicWalletMultiplier.ToString("F1")x</MudText>
</MudStack>
}
@if (package.IsBasePackage)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Star" Size="Size.Small" Color="Color.Warning" />
<MudText Typo="Typo.caption">پکیج پایه</MudText>
</MudStack>
}
</MudStack> </MudStack>
</MudStack> </MudStack>
</MudCardContent> </MudCardContent>
@@ -100,9 +128,9 @@
<MudGrid Spacing="4"> <MudGrid Spacing="4">
<MudItem xs="12" md="8"> <MudItem xs="12" md="8">
<MudStack Spacing="2"> <MudStack Spacing="2">
<MudText Typo="Typo.h5" Style="color: white;">چرا پکیج طلایی؟</MudText> <MudText Typo="Typo.h5" Style="color: white;">چرا پکیج بخریم؟</MudText>
<MudText Typo="Typo.body1" Style="color: rgba(255,255,255,0.9);"> <MudText Typo="Typo.body1" Style="color: rgba(255,255,255,0.9);">
با خرید پکیج طلایی، علاوه بر دسترسی به محصولات ویژه، می‌توانید تیم فروش خود را بسازید با خرید پکیج، علاوه بر دسترسی به محصولات ویژه، می‌توانید تیم فروش خود را بسازید
و از پاداش‌های هفتگی بهره‌مند شوید. هر چه تیم شما گسترده‌تر، درآمد شما بیشتر! و از پاداش‌های هفتگی بهره‌مند شوید. هر چه تیم شما گسترده‌تر، درآمد شما بیشتر!
</MudText> </MudText>
</MudStack> </MudStack>
@@ -64,36 +64,38 @@ public partial class PackageDetail : IDisposable
try try
{ {
// Load package details // Load package details via Customer RPC
var packageResponse = await PackageClient.GetPackageAsync(request: new() { Id = Id}, cancellationToken: _loadCts.Token); var response = await PackageClient.GetCustomerPackageDetailsAsync(
new GetCustomerPackageDetailsRequest { PackageId = Id },
cancellationToken: _loadCts.Token);
if (packageResponse != null) if (response != null)
{ {
// Build features from API response
var features = response.Features?.Select(f => f.Title).ToList() ?? new List<string>();
// Build specifications from API features (highlighted ones)
var specs = response.Features?
.Where(f => f.IsHighlighted)
.Select(f => new Specification
{
Name = f.Title,
Value = f.Description,
Icon = string.IsNullOrEmpty(f.Icon) ? Icons.Material.Filled.Star : f.Icon
}).ToList() ?? new List<Specification>();
_package = new PackageDetailDto _package = new PackageDetailDto
{ {
Id = packageResponse.Id, Id = response.Id,
Title = packageResponse.Title, Title = response.Title,
Body = packageResponse.Description, Body = response.Description,
Image = packageResponse.ImagePath ?? string.Empty, Image = response.ImagePath ?? string.Empty,
Specifications = new List<Specification> Specifications = specs,
{ Features = features,
new() { Name = "ظرفیت", Value = "تا ۲۰۰ عضو", Icon = Icons.Material.Filled.Group },
new() { Name = "شجره‌نامه", Value = "پیشرفته", Icon = Icons.Material.Filled.AccountTree },
new() { Name = "گزارش‌گیری", Value = "جامع", Icon = Icons.Material.Filled.Analytics },
new() { Name = "پشتیبانی", Value = "۲۴ ساعته", Icon = Icons.Material.Filled.Support }
},
Features = new List<string>
{
"مدیریت تیم نامحدود",
"شجره‌نامه بصری",
"محاسبه کارمزد خودکار",
"گزارش‌های مالی",
"پشتیبانی اولویت‌دار"
},
Pricing = new PricingInfo Pricing = new PricingInfo
{ {
OriginalPrice = packageResponse.Price, OriginalPrice = response.Price,
FinalPrice = packageResponse.Price, FinalPrice = response.Price,
HasDiscount = false, HasDiscount = false,
DiscountPercent = 0 DiscountPercent = 0
} }
@@ -44,11 +44,11 @@
Label="مبلغ (تومان)" Label="مبلغ (تومان)"
Variant="Variant.Outlined" Variant="Variant.Outlined"
Min="10_000" Min="10_000"
Max="100_000_000" Max="1_000_000_000"
Format="N0" Format="N0"
Adornment="Adornment.Start" Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Payments" AdornmentIcon="@Icons.Material.Filled.Payments"
HelperText="حداقل ۱۰,۰۰۰ و حداکثر ۱۰۰,۰۰۰,۰۰۰ تومان" /> HelperText="حداقل ۱۰,۰۰۰ و حداکثر ۱,۰۰۰,۰۰۰,۰۰۰ تومان" />
@* دکمه‌های مبلغ سریع *@ @* دکمه‌های مبلغ سریع *@
<MudStack Row="true" Spacing="1" Class="flex-wrap"> <MudStack Row="true" Spacing="1" Class="flex-wrap">
@@ -29,10 +29,8 @@ public partial class ChargeDiscountWallet : ComponentBase
try try
{ {
// تبدیل تومان به ریال // مبلغ به تومان — CMS خودش موقع ارسال به درگاه ×۱۰ می‌کنه
var amountInRial = _chargeAmount * 10; var (success, gatewayUrl, error) = await WalletService.InitiateDiscountChargeAsync(_chargeAmount);
var (success, gatewayUrl, error) = await WalletService.InitiateDiscountChargeAsync(amountInRial);
if (success && !string.IsNullOrEmpty(gatewayUrl)) if (success && !string.IsNullOrEmpty(gatewayUrl))
{ {
+2 -86
View File
@@ -119,7 +119,7 @@
لینک دعوت شما هنوز فعال نشده است لینک دعوت شما هنوز فعال نشده است
</MudText> </MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-1" Style="max-width:560px; margin:0 auto;"> <MudText Typo="Typo.body2" Class="mud-text-secondary mb-1" Style="max-width:560px; margin:0 auto;">
برای فعال‌سازی لینک دعوت، ابتدا <strong>پکیج پایه</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید. برای فعال‌سازی لینک دعوت، ابتدا یکی از <strong>پکیج‌ها</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
</MudText> </MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-4" Style="max-width:560px; margin:0 auto;"> <MudText Typo="Typo.body2" Class="mud-text-secondary mb-4" Style="max-width:560px; margin:0 auto;">
از مزایای ویژه عضویت بهره‌مند شده و با فعالیت در زمینه توسعه فروشگاه‌ها پاداش دریافت کنید. از مزایای ویژه عضویت بهره‌مند شده و با فعالیت در زمینه توسعه فروشگاه‌ها پاداش دریافت کنید.
@@ -130,7 +130,7 @@
Class="rounded-pill" Class="rounded-pill"
StartIcon="@Icons.Material.Filled.ShoppingCart" StartIcon="@Icons.Material.Filled.ShoppingCart"
OnClick="OpenPurchaseOptions"> OnClick="OpenPurchaseOptions">
شروع فرآیند تامین اعتبار تهیه پکیج
</MudButton> </MudButton>
</MudPaper> </MudPaper>
} }
@@ -195,88 +195,4 @@
</MudGrid> </MudGrid>
</MudContainer> </MudContainer>
@* ── Purchase Options Dialog ── *@
<MudDialog @bind-Visible="_showPurchaseBottomSheet">
<DialogContent>
<MudStack Spacing="3" Class="pa-2">
<MudText Typo="Typo.h6" Align="Align.Center">
خرید پکیج پایه ۵۶ میلیون تومان
</MudText>
<MudDivider />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
برای خرید پکیج پایه، یکی از روش‌های زیر را انتخاب کنید:
</MudText>
<MudPaper Elevation="0" Class="pa-4 rounded-lg" Style="border: 2px solid var(--mud-palette-primary);">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Color="Color.Primary" Size="Size.Medium" />
<MudText Typo="Typo.subtitle1" Color="Color.Primary"><b>پرداخت مستقیم</b></MudText>
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary">پرداخت آنی از طریق درگاه بانکی</MudText>
@if (_purchaseCycleCount > 0)
{
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
Disabled="@_isProcessingPayment"
StartIcon="@Icons.Material.Filled.Payment"
OnClick="DirectPayment">
@if (_isProcessingPayment)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
<span>در حال انتقال به درگاه...</span>
}
else
{
<span>پرداخت با کارت بانکی</span>
}
</MudButton>
}
else
{
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
Disabled="@_isProcessingPayment"
StartIcon="@Icons.Material.Filled.Payment"
OnClick="DirectPayment">
@if (_isProcessingPayment)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
<span>در حال انتقال به درگاه...</span>
}
else
{
<span>پرداخت با کارت بانکی</span>
}
</MudButton>
}
</MudStack>
</MudPaper>
@if (_purchaseCycleCount == 0)
{
<MudPaper Elevation="0" Class="pa-4 rounded-lg" Style="border: 2px solid var(--mud-palette-tertiary);">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Diamond" Color="Color.Tertiary" Size="Size.Medium" />
<MudText Typo="Typo.subtitle1" Color="Color.Tertiary"><b>اعتبار الماسی دایا</b></MudText>
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary">تأمین اعتبار از طریق خرید توکن الماس و دریافت وام</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Tertiary" FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalance" OnClick="DayaLoanPayment">
تأمین اعتبار الماسی
</MudButton>
</MudStack>
</MudPaper>
}
else
{
<MudAlert Severity="Severity.Info" Dense="true" Class="mt-1">
از دور دوم به بعد، خرید پکیج فقط از طریق درگاه بانکی امکان‌پذیر است.
</MudAlert>
}
<MudButton Variant="Variant.Text" Color="Color.Default" FullWidth="true" OnClick="ClosePurchaseOptions">
بستن
</MudButton>
</MudStack>
</DialogContent>
</MudDialog>
@@ -42,8 +42,7 @@ public partial class Index
private bool _isClubMemberActive; private bool _isClubMemberActive;
private bool CanShowReferralLink => _hasPurchasedPackage && _isClubMemberActive; private bool CanShowReferralLink => _hasPurchasedPackage && _isClubMemberActive;
// Purchase Options Bottom Sheet // Purchase flow
private bool _showPurchaseBottomSheet;
private bool _isProcessingPayment; private bool _isProcessingPayment;
private int _purchaseCycleCount; // تعداد دورهای خرید — اگر > 0 فقط IPG مجاز private int _purchaseCycleCount; // تعداد دورهای خرید — اگر > 0 فقط IPG مجاز
@@ -55,7 +54,7 @@ public partial class Index
new(RouteConstants.Profile.Tree, Icons.Material.Filled.AccountTree, "شجره‌نامه", "ساختار تیم", "background:rgba(99,102,241,.12); color:#6366f1;"), new(RouteConstants.Profile.Tree, Icons.Material.Filled.AccountTree, "شجره‌نامه", "ساختار تیم", "background:rgba(99,102,241,.12); color:#6366f1;"),
new(RouteConstants.Gateway.StoreChooser, Icons.Material.Filled.Store, "فروشگاه‌ها", "ورود به فروشگاه", "background:rgba(16,185,129,.12); color:#10b981;"), new(RouteConstants.Gateway.StoreChooser, Icons.Material.Filled.Store, "فروشگاه‌ها", "ورود به فروشگاه", "background:rgba(16,185,129,.12); color:#10b981;"),
new(RouteConstants.Profile.Wallet, Icons.Material.Filled.AccountBalanceWallet, "کیف پول", "مدیریت و تاریخچه", "background:rgba(16,185,129,.12); color:#10b981;"), new(RouteConstants.Profile.Wallet, Icons.Material.Filled.AccountBalanceWallet, "کیف پول", "مدیریت و تاریخچه", "background:rgba(16,185,129,.12); color:#10b981;"),
new(RouteConstants.Profile.MagicWallet, Icons.Material.Filled.AutoAwesome, "کیف‌پول جادویی", "شارژ ×2.5", "background:rgba(168,85,247,.12); color:#a855f7;"), new(RouteConstants.Profile.MagicWallet, Icons.Material.Filled.AutoAwesome, "کیف‌پول جادویی", "شارژ چند‌برابری", "background:rgba(168,85,247,.12); color:#a855f7;"),
new(RouteConstants.Profile.WithdrawalRequests, Icons.Material.Filled.RequestPage, "درخواست برداشت", "ثبت و پیگیری", "background:rgba(245,158,11,.12); color:#f59e0b;"), new(RouteConstants.Profile.WithdrawalRequests, Icons.Material.Filled.RequestPage, "درخواست برداشت", "ثبت و پیگیری", "background:rgba(245,158,11,.12); color:#f59e0b;"),
new(RouteConstants.Club.Membership, Icons.Material.Filled.CardMembership, "باشگاه مشتریان", "عضویت و مزایا", "background:rgba(168,85,247,.12); color:#a855f7;"), new(RouteConstants.Club.Membership, Icons.Material.Filled.CardMembership, "باشگاه مشتریان", "عضویت و مزایا", "background:rgba(168,85,247,.12); color:#a855f7;"),
new(RouteConstants.Network.Statistics, Icons.Material.Filled.Groups, "آمار باشگاه", "جزئیات باشگاه", "background:rgba(59,130,246,.12); color:#3b82f6;"), new(RouteConstants.Network.Statistics, Icons.Material.Filled.Groups, "آمار باشگاه", "جزئیات باشگاه", "background:rgba(59,130,246,.12); color:#3b82f6;"),
@@ -394,32 +393,54 @@ public partial class Index
} }
} }
private void OpenPurchaseOptions() private async Task OpenPurchaseOptions()
{ {
_showPurchaseBottomSheet = true; var options = new DialogOptions
{
MaxWidth = MaxWidth.Small,
FullWidth = true,
CloseOnEscapeKey = true,
BackdropClick = true
};
var parameters = new DialogParameters<Shared.PackagePurchaseDialog>
{
{ x => x.PurchaseCycleCount, _purchaseCycleCount }
};
var dialog = await DialogService.ShowAsync<Shared.PackagePurchaseDialog>(
string.Empty, parameters, options);
var result = await dialog.Result;
if (result.Canceled) return;
if (result.Data is Shared.PackagePurchaseDialog.PackagePurchaseResult purchase)
{
switch (purchase.Method)
{
case Shared.PackagePurchaseDialog.PaymentMethodType.DirectPayment:
await ProcessDirectPayment(purchase.Package.Id);
break;
case Shared.PackagePurchaseDialog.PaymentMethodType.DayaLoan:
await ProcessDayaLoanPayment();
break;
}
}
} }
private void ClosePurchaseOptions() private async Task ProcessDirectPayment(long packageId)
{
_showPurchaseBottomSheet = false;
}
private async Task DirectPayment()
{ {
if (_isProcessingPayment) return; if (_isProcessingPayment) return;
_isProcessingPayment = true; _isProcessingPayment = true;
_showPurchaseBottomSheet = false; StateHasChanged();
try try
{ {
// Create callback URL for payment verification var response = await PackageContract.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
var callbackUrl = $"{Navigation.BaseUri}profile/payment-callback";
// Call BFF to initiate payment (UserId is taken from JWT token in BFF)
var response = await PackageContract.InitiateBasePackagePaymentAsync(new InitiateBasePackagePaymentRequest
{ {
CallbackUrl = callbackUrl PackageId = packageId,
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
}); });
if (!response.Success) if (!response.Success)
@@ -428,7 +449,6 @@ public partial class Index
return; return;
} }
// Redirect to payment gateway
if (!string.IsNullOrEmpty(response.PaymentGatewayUrl)) if (!string.IsNullOrEmpty(response.PaymentGatewayUrl))
{ {
Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true); Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true);
@@ -449,41 +469,34 @@ public partial class Index
} }
} }
private async Task DayaLoanPayment() private async Task ProcessDayaLoanPayment()
{ {
// Validate user info before redirecting
if (string.IsNullOrWhiteSpace(_updateUserRequest.FirstName)) if (string.IsNullOrWhiteSpace(_updateUserRequest.FirstName))
{ {
_showPurchaseBottomSheet = false; Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (نام وارد نشده)", Severity.Error);
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (نام وارد نشده)", Severity.Error);
return; return;
} }
if (string.IsNullOrWhiteSpace(_updateUserRequest.LastName)) if (string.IsNullOrWhiteSpace(_updateUserRequest.LastName))
{ {
_showPurchaseBottomSheet = false; Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (نام خانوادگی وارد نشده)", Severity.Error);
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (نام خانوادگی وارد نشده)", Severity.Error);
return; return;
} }
if (string.IsNullOrWhiteSpace(_updateUserRequest.NationalCode)) if (string.IsNullOrWhiteSpace(_updateUserRequest.NationalCode))
{ {
_showPurchaseBottomSheet = false; Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (کدملی وارد نشده)", Severity.Error);
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (کدملی وارد نشده)", Severity.Error);
return; return;
} }
if (_updateUserRequest.BirthDate == null) if (_updateUserRequest.BirthDate == null)
{ {
_showPurchaseBottomSheet = false; Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
return; return;
} }
if (!_addresses.Any()) if (!_addresses.Any())
{ {
_showPurchaseBottomSheet = false; Snackbar.Add("آدرس محل سکونت شما الزامی است!", Severity.Error);
Snackbar.Add($"آدرس محل سکونت شما الزامی است!", Severity.Error);
return; return;
} }
_showPurchaseBottomSheet = false;
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364"; var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
await JSRuntime.InvokeVoidAsync("open", url, "_blank"); await JSRuntime.InvokeVoidAsync("open", url, "_blank");
} }
@@ -36,6 +36,11 @@
{ {
@* ═══ حالت جادویی فعال ═══ *@ @* ═══ حالت جادویی فعال ═══ *@
@* ── G4: هشدار تنظیمات پکیج‌بیس (Q28) ── *@
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Warning">
سقف واریز و ضریب اعتبار کیف‌پول جادویی بر اساس پکیج شما تنظیم شده است. پکیج‌های بالاتر سقف و ضریب بیشتری دارند.
</MudAlert>
@* ─── وضعیت و پیشرفت ─── *@ @* ─── وضعیت و پیشرفت ─── *@
<MudPaper Elevation="2" Class="pa-5 rounded-xl" Style="background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%); color: white;"> <MudPaper Elevation="2" Class="pa-5 rounded-xl" Style="background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%); color: white;">
<MudStack Spacing="2"> <MudStack Spacing="2">
@@ -92,8 +97,8 @@
</MudText> </MudText>
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info"> <MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
مبلغ واریزی شما <strong>×۲.۵</strong> به موجودی اضافه می‌شود. مبلغ واریزی شما <strong>×@_status.MagicMultiplier.ToString("0.#")</strong> به موجودی اضافه می‌شود.
مثلاً ۱۰ میلیون واریز = ۲۵ میلیون اعتبار. مثلاً ۱۰ میلیون واریز = @FormatToman((long)(10_000_000 * (decimal)_status.MagicMultiplier)) اعتبار.
</MudAlert> </MudAlert>
<MudStack Spacing="2"> <MudStack Spacing="2">
@@ -101,7 +106,7 @@
Label="مبلغ واریز (تومان)" Label="مبلغ واریز (تومان)"
Variant="Variant.Outlined" Variant="Variant.Outlined"
Min="10_000" Min="10_000"
Max="@(_status.MagicRemainingDeposit / 10)" Max="@_status.MagicRemainingDeposit"
Format="N0" Format="N0"
Adornment="Adornment.Start" Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Payments" AdornmentIcon="@Icons.Material.Filled.Payments"
@@ -115,8 +120,8 @@
<MudText Typo="Typo.body2">@FormatToman(_chargeAmount)</MudText> <MudText Typo="Typo.body2">@FormatToman(_chargeAmount)</MudText>
</MudStack> </MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween"> <MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success"><strong>اعتبار دریافتی (×۲.۵):</strong></MudText> <MudText Typo="Typo.body2" Color="Color.Success"><strong>اعتبار دریافتی (×@_status.MagicMultiplier.ToString("0.#")):</strong></MudText>
<MudText Typo="Typo.body2" Color="Color.Success"><strong>@FormatToman((long)(_chargeAmount * 2.5m))</strong></MudText> <MudText Typo="Typo.body2" Color="Color.Success"><strong>@FormatToman((long)(_chargeAmount * (decimal)_status.MagicMultiplier))</strong></MudText>
</MudStack> </MudStack>
</MudPaper> </MudPaper>
} }
@@ -125,7 +130,7 @@
<MudStack Row="true" Spacing="1" Class="flex-wrap"> <MudStack Row="true" Spacing="1" Class="flex-wrap">
@foreach (var preset in _presetAmounts) @foreach (var preset in _presetAmounts)
{ {
if (preset * 10 <= _status.MagicRemainingDeposit) if (preset <= _status.MagicRemainingDeposit)
{ {
<MudButton Variant="Variant.Outlined" Size="Size.Small" <MudButton Variant="Variant.Outlined" Size="Size.Small"
Color="@(_chargeAmount == preset ? Color.Primary : Color.Default)" Color="@(_chargeAmount == preset ? Color.Primary : Color.Default)"
@@ -171,13 +176,13 @@
<MudExpansionPanel Text="قوانین کیف‌پول جادویی" MaxHeight="500" IsInitiallyExpanded="false"> <MudExpansionPanel Text="قوانین کیف‌پول جادویی" MaxHeight="500" IsInitiallyExpanded="false">
<MudList T="string" Dense="true"> <MudList T="string" Dense="true">
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success"> <MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
هر مبلغ واریزی ×۲.۵ به موجودی اضافه می‌شود هر مبلغ واریزی ×@_status.MagicMultiplier.ToString("0.#") به موجودی اضافه می‌شود
</MudListItem> </MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success"> <MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
سقف واریز در هر دور: ۱۰۰ میلیون تومان سقف واریز در هر دور: @FormatPrice(_status.MagicMaxDeposit)
</MudListItem> </MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success"> <MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
حداکثر اعتبار: ۲۵۰ میلیون تومان (سود ۱۵۰٪) حداکثر اعتبار: @FormatPrice(_status.MagicMaxCredit)
</MudListItem> </MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Warning" IconColor="Color.Warning"> <MudListItem T="string" Icon="@Icons.Material.Filled.Warning" IconColor="Color.Warning">
در حالت جادویی، کمیسیون و پاداش تیمی غیرفعال است در حالت جادویی، کمیسیون و پاداش تیمی غیرفعال است
@@ -46,10 +46,8 @@ public partial class MagicWallet : ComponentBase
try try
{ {
// تبدیل تومان به ریال // مبلغ به تومان — CMS خودش موقع ارسال به درگاه ×۱۰ می‌کنه
var amountInRial = _chargeAmount * 10; var (success, gatewayUrl, error) = await WalletService.InitiateMagicChargeAsync(_chargeAmount);
var (success, gatewayUrl, error) = await WalletService.InitiateMagicChargeAsync(amountInRial);
if (success && !string.IsNullOrEmpty(gatewayUrl)) if (success && !string.IsNullOrEmpty(gatewayUrl))
{ {
@@ -71,8 +69,8 @@ public partial class MagicWallet : ComponentBase
} }
} }
private static string FormatPrice(long priceInRial) private static string FormatPrice(long price)
=> string.Format("{0:N0} تومان", priceInRial / 10); => string.Format("{0:N0} تومان", price);
private static string FormatToman(long toman) private static string FormatToman(long toman)
=> string.Format("{0:N0} تومان", toman); => string.Format("{0:N0} تومان", toman);
@@ -3,7 +3,7 @@
@using Blazored.LocalStorage @using Blazored.LocalStorage
@using CMSMicroservice.Protobuf.Protos.Package @using CMSMicroservice.Protobuf.Protos.Package
@using CMSMicroservice.Protobuf.Protos.User @using CMSMicroservice.Protobuf.Protos.User
@inject IJSRuntime JS @using FrontOffice.Main.Utilities
<PageTitle>نتیجه پرداخت | کارا بازار سلامت</PageTitle> <PageTitle>نتیجه پرداخت | کارا بازار سلامت</PageTitle>
@@ -21,10 +21,10 @@
<MudText Typo="Typo.h5" Color="Color.Success" Class="mt-4">پرداخت موفق</MudText> <MudText Typo="Typo.h5" Color="Color.Success" Class="mt-4">پرداخت موفق</MudText>
<MudText Typo="Typo.body1" Class="mt-2">@_message</MudText> <MudText Typo="Typo.body1" Class="mt-2">@_message</MudText>
@if (!string.IsNullOrEmpty(_refId)) @if (_transactionId > 0)
{ {
<MudText Typo="Typo.body2" Class="mt-3"> <MudText Typo="Typo.body2" Class="mt-3">
<strong>کد پیگیری:</strong> @_refId <strong>کد ارجاعی:</strong> @_transactionId
</MudText> </MudText>
} }
@@ -33,13 +33,10 @@
<MudText Typo="Typo.body2" Color="Color.Secondary"> <MudText Typo="Typo.body2" Color="Color.Secondary">
موجودی کیف پول اصلی: @FormatPrice(_walletBalance) موجودی کیف پول اصلی: @FormatPrice(_walletBalance)
</MudText> </MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">
موجودی کیف پول اعتباری: @FormatPrice(_discountBalance)
</MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6" <MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
OnClick="GoBack"> Href="@_successReturnUrl">
بازگشت به پروفایل @_successReturnText
</MudButton> </MudButton>
} }
else else
@@ -49,8 +46,8 @@
<MudText Typo="Typo.body1" Class="mt-2">@_message</MudText> <MudText Typo="Typo.body1" Class="mt-2">@_message</MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6" <MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
OnClick="GoBack"> Href="@_failReturnUrl">
بازگشت به پروفایل @_failReturnText
</MudButton> </MudButton>
<MudButton Color="Color.Secondary" Variant="Variant.Outlined" Class="mt-3 mr-2" <MudButton Color="Color.Secondary" Variant="Variant.Outlined" Class="mt-3 mr-2"
@@ -64,12 +61,19 @@
@code { @code {
[Inject] private PackageContract.PackageContractClient PackageContractClient { get; set; } = default!; [Inject] private PackageContract.PackageContractClient PackageContractClient { get; set; } = default!;
[Inject] private UserContract.UserContractClient UserContractClient { get; set; } = default!; [Inject] private UserContract.UserContractClient UserContractClient { get; set; } = default!;
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!; [Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!; [Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private NavigationManager NavManager { get; set; } = default!; [Inject] private NavigationManager NavManager { get; set; } = default!;
private const string TokenStorageKey = "auth:token"; private const string TokenStorageKey = "auth:token";
/// <summary>
/// نوع پرداخت: package (پیش‌فرض) | magic-wallet | discount-wallet | discount-order
/// </summary>
[SupplyParameterFromQuery(Name = "type")]
private string? PaymentType { get; set; }
[SupplyParameterFromQuery(Name = "orderId")] [SupplyParameterFromQuery(Name = "orderId")]
private long OrderId { get; set; } private long OrderId { get; set; }
@@ -85,9 +89,14 @@
private bool _isLoading = true; private bool _isLoading = true;
private bool _isSuccess; private bool _isSuccess;
private string _message = string.Empty; private string _message = string.Empty;
private string? _refId; private long _transactionId;
private long _walletBalance; private long _walletBalance;
private long _discountBalance;
// دکمه‌های بازگشت بسته به نوع پرداخت
private string _successReturnUrl = "/profile";
private string _successReturnText = "بازگشت به پروفایل";
private string _failReturnUrl = "/profile";
private string _failReturnText = "بازگشت به پروفایل";
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
@@ -101,35 +110,23 @@
{ {
try try
{ {
if (OrderId <= 0 || TransactionId <= 0 || string.IsNullOrEmpty(Authority)) // تعیین نوع پرداخت — اگر type نباشد، پیش‌فرض خرید پکیج
{ var type = PaymentType?.ToLowerInvariant() ?? "package";
_isSuccess = false;
_message = "پارامترهای پرداخت نامعتبر است";
_isLoading = false;
StateHasChanged();
return;
}
var response = await PackageContractClient.VerifyBasePackagePaymentAsync(new VerifyBasePackagePaymentRequest switch (type)
{ {
OrderId = OrderId, case "magic-wallet":
TransactionId = TransactionId, await VerifyMagicWalletCharge();
PaymentSuccess = Status == "OK", break;
RefId = !string.IsNullOrEmpty(Authority) ? Authority : null, case "discount-wallet":
Message = !string.IsNullOrEmpty(Status) ? Status : null await VerifyDiscountWalletCharge();
}); break;
case "discount-order":
_isSuccess = response.Success; await VerifyDiscountOrderPayment();
_message = response.Message; break;
_refId = response.ReferenceCode ?? ""; default: // "package" or missing
_walletBalance = response.WalletBalance; await VerifyPackagePurchase();
_discountBalance = response.DiscountBalance; break;
// اگر پرداخت موفق بود، توکن را refresh می‌کنیم
// چون PackagePurchaseMethod تغییر کرده و باید در claims جدید باشد
if (_isSuccess)
{
await RefreshTokenAsync();
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -144,9 +141,155 @@
} }
} }
/// <summary>
/// تأیید خرید پکیج — رفتار فعلی
/// </summary>
private async Task VerifyPackagePurchase()
{
_successReturnUrl = "/profile";
_successReturnText = "بازگشت به پروفایل";
_failReturnUrl = "/profile";
_failReturnText = "بازگشت به پروفایل";
if (OrderId <= 0 || string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "پارامترهای پرداخت نامعتبر است";
return;
}
var response = await PackageContractClient.CustomerVerifyPackagePurchaseAsync(new CustomerVerifyPackagePurchaseRequest
{
OrderId = OrderId,
Authority = Authority ?? string.Empty,
Status = Status ?? string.Empty
});
_isSuccess = response.Success;
_message = response.Message;
_transactionId = response.TransactionId;
if (_isSuccess)
{
await UpdateWalletBalanceAndRefreshToken();
}
}
/// <summary>
/// تأیید شارژ کیف‌پول جادویی
/// </summary>
private async Task VerifyMagicWalletCharge()
{
_successReturnUrl = "/profile/magic-wallet";
_successReturnText = "بازگشت به کیف پول جادویی";
_failReturnUrl = "/profile/magic-wallet";
_failReturnText = "بازگشت به کیف پول جادویی";
if (string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "کد Authority نامعتبر است";
return;
}
var (success, message) = await WalletService.VerifyMagicChargeAsync(
Authority, Status ?? "NOK");
_isSuccess = success;
_message = message;
if (_isSuccess)
{
await UpdateWalletBalance();
}
}
/// <summary>
/// تأیید شارژ کیف پول تخفیفی
/// </summary>
private async Task VerifyDiscountWalletCharge()
{
_successReturnUrl = "/profile/charge-discount-wallet";
_successReturnText = "بازگشت به کیف پول تخفیفی";
_failReturnUrl = "/profile/charge-discount-wallet";
_failReturnText = "بازگشت به کیف پول تخفیفی";
if (string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "کد Authority نامعتبر است";
return;
}
var (success, message) = await WalletService.VerifyDiscountChargeAsync(
Authority, Status ?? "NOK");
_isSuccess = success;
_message = message;
if (_isSuccess)
{
await UpdateWalletBalance();
}
}
/// <summary>
/// تأیید پرداخت سفارش فروشگاه تخفیفی
/// </summary>
private async Task VerifyDiscountOrderPayment()
{
_successReturnUrl = $"/discount-store/order/{OrderId}";
_successReturnText = "مشاهده سفارش";
_failReturnUrl = $"/discount-store/order/{OrderId}";
_failReturnText = "مشاهده سفارش";
if (OrderId <= 0 || string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "پارامترهای پرداخت نامعتبر است";
return;
}
var (success, message, _) = await DiscountOrderService.VerifyDiscountOrderPaymentAsync(
OrderId, Authority, Status ?? "NOK");
_isSuccess = success;
_message = message;
}
/// <summary>
/// بروزرسانی موجودی کیف پول
/// </summary>
private async Task UpdateWalletBalance()
{
try
{
var balances = await WalletService.GetBalancesAsync();
_walletBalance = balances.CreditBalance;
}
catch { /* اگر خطا شد، صفر نمایش بده */ }
}
/// <summary>
/// بروزرسانی موجودی + refresh توکن (فقط برای خرید پکیج)
/// </summary>
private async Task UpdateWalletBalanceAndRefreshToken()
{
await UpdateWalletBalance();
await RefreshTokenAsync();
}
private void RetryPayment() private void RetryPayment()
{ {
NavManager.NavigateTo("/profile"); var type = PaymentType?.ToLowerInvariant() ?? "package";
var url = type switch
{
"magic-wallet" => "/profile/magic-wallet",
"discount-wallet" => "/profile/charge-discount-wallet",
"discount-order" => "/discount-store",
_ => "/profile"
};
NavManager.NavigateTo(url, forceLoad: true);
} }
/// <summary> /// <summary>
@@ -174,6 +317,4 @@
} }
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
} }
@@ -112,6 +112,30 @@ else
</MudStack> </MudStack>
} }
</MudStack> </MudStack>
@* نمایش PV سفارش *@
@if (_pvData != null && _pvData.TotalPv > 0)
{
<MudDivider Class="my-2" />
<MudStack Spacing="1" Class="pa-2" Style="background-color: var(--mud-palette-background-grey); border-radius: 8px;">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.TrendingUp" Color="Color.Info" Size="Size.Small" />
<MudText Typo="Typo.subtitle2" Color="Color.Info">امتیاز فروش (PV)</MudText>
</MudStack>
@foreach (var product in _pvData.Products)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Class="mud-text-secondary">@product.ProductTitle × @product.Quantity</MudText>
<MudText Typo="Typo.caption">@string.Format("{0:N0}", product.TotalPv) PV</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="fw-bold">جمع PV:</MudText>
<MudText Typo="Typo.body2" Color="Color.Info" Class="fw-bold">@string.Format("{0:N0}", _pvData.TotalPv) PV</MudText>
</MudStack>
</MudStack>
}
</MudStack> </MudStack>
</MudPaper> </MudPaper>
</MudContainer> </MudContainer>
@@ -12,12 +12,17 @@ public partial class OrderDetail : ComponentBase
[Parameter] public long id { get; set; } [Parameter] public long id { get; set; }
private GetUserOrderResponse? _order; private GetUserOrderResponse? _order;
private CalculateOrderPVResponse? _pvData;
private bool _loading; private bool _loading;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_loading = true; _loading = true;
_order = await OrderService.GetOrderAsync(id); _order = await OrderService.GetOrderAsync(id);
if (_order != null)
{
_pvData = await OrderService.CalculateOrderPVAsync(id);
}
_loading = false; _loading = false;
} }
@@ -9,6 +9,7 @@
@inject AuthService AuthService @inject AuthService AuthService
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject PackageService PackageService
<MudDialog DisableSidePadding="true"> <MudDialog DisableSidePadding="true">
<DialogContent> <DialogContent>
@@ -123,6 +124,23 @@
private readonly Guid _signGuid = Guid.NewGuid(); private readonly Guid _signGuid = Guid.NewGuid();
private System.Timers.Timer? _timer; private System.Timers.Timer? _timer;
private const string TokenStorageKey = "auth:token"; private const string TokenStorageKey = "auth:token";
private string _packageDisplayName = "پکیج پایه";
private string _packageDisplayPrice = "۵۶ میلیون تومان";
protected override async Task OnInitializedAsync()
{
try
{
var packages = await PackageService.GetAllPackagesAsync();
var basePackage = packages.FirstOrDefault(p => p.IsBasePackage) ?? packages.FirstOrDefault();
if (basePackage != null)
{
_packageDisplayName = basePackage.Title;
_packageDisplayPrice = $"{basePackage.Price:N0} تومان";
}
}
catch { /* fallback to defaults */ }
}
// این Modal غیرقابل بسته شدن است - Options در ShowAsync تنظیم می‌شوند // این Modal غیرقابل بسته شدن است - Options در ShowAsync تنظیم می‌شوند
@@ -242,7 +260,7 @@
<p>۱-۳. پاداش معرفی: مبلغی که به ازای هر معرفی موفق به کاربر تعلق می‌گیرد.</p> <p>۱-۳. پاداش معرفی: مبلغی که به ازای هر معرفی موفق به کاربر تعلق می‌گیرد.</p>
<p><strong>ماده ۲ - شرایط عضویت</strong></p> <p><strong>ماده ۲ - شرایط عضویت</strong></p>
<p>۲-۱. کاربر با پرداخت مبلغ ۵۶ میلیون تومان (پکیج پایه)، امکان عضویت در باشگاه مشتریان را خواهد داشت.</p> <p>۲-۱. کاربر با پرداخت مبلغ {_packageDisplayPrice} ({_packageDisplayName})، امکان عضویت در باشگاه مشتریان را خواهد داشت.</p>
<p>۲-۲. پس از امضای این قرارداد، لینک دعوت کاربر فعال خواهد شد.</p> <p>۲-۲. پس از امضای این قرارداد، لینک دعوت کاربر فعال خواهد شد.</p>
<p>۲-۳. کاربر می‌تواند حداکثر ۲ نفر را مستقیماً دعوت کند (یک نفر در تیم اول و یک نفر در تیم دوم).</p> <p>۲-۳. کاربر می‌تواند حداکثر ۲ نفر را مستقیماً دعوت کند (یک نفر در تیم اول و یک نفر در تیم دوم).</p>
@@ -0,0 +1,234 @@
@using FrontOffice.Main.Utilities
@inject PackageService PackageService
@inject ISnackbar Snackbar
<MudDialog>
<TitleContent>
@if (_selectedPackage != null)
{
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIconButton Icon="@Icons.Material.Filled.ArrowForward"
Size="Size.Small"
OnClick="BackToList" />
<MudText Typo="Typo.h6">انتخاب روش پرداخت</MudText>
</MudStack>
}
else
{
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Color="Color.Primary" />
<MudText Typo="Typo.h6">انتخاب پکیج</MudText>
</MudStack>
}
</TitleContent>
<DialogContent>
@if (_isLoading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">در حال بارگذاری پکیج‌ها...</MudText>
</MudStack>
}
else if (_selectedPackage != null)
{
@* ── Step 2: Payment method selection ── *@
<MudStack Spacing="3">
@* Selected Package Summary *@
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="background:rgba(99,102,241,.06);">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudAvatar Size="Size.Medium" Style="background:rgba(99,102,241,.15); color:#6366f1;">
<MudIcon Icon="@Icons.Material.Filled.Inventory2" />
</MudAvatar>
<MudStack Spacing="0" Class="flex-grow-1">
<MudText Typo="Typo.subtitle1"><b>@_selectedPackage.Title</b></MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@_selectedPackage.FormattedPrice</MudText>
</MudStack>
</MudStack>
</MudPaper>
<MudDivider />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
روش پرداخت خود را انتخاب کنید:
</MudText>
@* Direct Payment — always available *@
<MudPaper Elevation="0" Class="pa-4 rounded-lg pkg-payment-option"
Style="border:2px solid var(--mud-palette-primary); cursor:pointer;"
@onclick="() => SelectPaymentMethod(PaymentMethodType.DirectPayment)">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Color="Color.Primary" Size="Size.Medium" />
<MudStack Spacing="0" Class="flex-grow-1">
<MudText Typo="Typo.subtitle1" Color="Color.Primary"><b>پرداخت مستقیم</b></MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">پرداخت آنی از طریق درگاه بانکی</MudText>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ArrowBack" Color="Color.Primary" Size="Size.Small" />
</MudStack>
</MudStack>
</MudPaper>
@* Daya Loan — only for base package AND first purchase cycle *@
@if (_selectedPackage.SupportsDayaPurchase && PurchaseCycleCount == 0)
{
<MudPaper Elevation="0" Class="pa-4 rounded-lg pkg-payment-option"
Style="border:2px solid var(--mud-palette-tertiary); cursor:pointer;"
@onclick="() => SelectPaymentMethod(PaymentMethodType.DayaLoan)">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Diamond" Color="Color.Tertiary" Size="Size.Medium" />
<MudStack Spacing="0" Class="flex-grow-1">
<MudText Typo="Typo.subtitle1" Color="Color.Tertiary"><b>اعتبار الماسی دایا</b></MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">تأمین اعتبار از طریق خرید توکن الماس</MudText>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ArrowBack" Color="Color.Tertiary" Size="Size.Small" />
</MudStack>
</MudStack>
</MudPaper>
}
else if (!_selectedPackage.SupportsDayaPurchase)
{
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="mt-1">
اعتبار الماسی دایا فقط برای پکیج پایه قابل استفاده است.
</MudAlert>
}
else if (PurchaseCycleCount > 0)
{
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="mt-1">
از دور دوم به بعد، خرید پکیج فقط از طریق درگاه بانکی امکان‌پذیر است.
</MudAlert>
}
</MudStack>
}
else
{
@* ── Step 1: Package tile grid ── *@
<MudStack Spacing="3">
<MudText Typo="Typo.body2" Class="mud-text-secondary">
پکیج مورد نظر خود را انتخاب کنید:
</MudText>
<MudGrid Spacing="3">
@foreach (var pkg in _packages)
{
<MudItem xs="12" sm="6" md="@(_packages.Count <= 2 ? 6 : 4)">
<MudPaper Elevation="0" Class="pa-4 rounded-xl pkg-tile text-center"
Style="@GetTileStyle(pkg)"
@onclick="() => SelectPackage(pkg)">
@if (pkg.IsBasePackage)
{
<MudChip T="string" Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled"
Class="pkg-tile-badge">پایه</MudChip>
}
<MudStack Spacing="1" AlignItems="AlignItems.Center">
<MudAvatar Size="Size.Large" Style="@GetAvatarStyle(pkg)" Class="mb-1">
<MudIcon Icon="@Icons.Material.Filled.Inventory2" Size="Size.Large" />
</MudAvatar>
<MudText Typo="Typo.subtitle1"><b>@pkg.Title</b></MudText>
<MudText Typo="Typo.h6" Color="Color.Success">@pkg.FormattedPrice</MudText>
@if (!string.IsNullOrWhiteSpace(pkg.Description))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary"
Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">
@((MarkupString)pkg.Description)
</MudText>
}
@* Key features *@
<MudStack Spacing="0" Class="mt-2" Style="width:100%;">
@if (pkg.MagicWalletMultiplier > 0)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.AutoAwesome" Size="Size.Small" Class="me-1" Style="font-size:.85rem;vertical-align:middle;" />
ضریب جادویی: @pkg.MagicWalletMultiplier.ToString("F1")x
</MudText>
}
@if (pkg.DiscountMultiplier > 0)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.Discount" Size="Size.Small" Class="me-1" Style="font-size:.85rem;vertical-align:middle;" />
ضریب اعتبار: @pkg.DiscountMultiplier.ToString("F1")x
</MudText>
}
@if (pkg.SupportsDayaPurchase)
{
<MudText Typo="Typo.caption" Color="Color.Tertiary">
<MudIcon Icon="@Icons.Material.Filled.Diamond" Size="Size.Small" Class="me-1" Style="font-size:.85rem;vertical-align:middle;" />
قابل خرید با دایا
</MudText>
}
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudStack>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Variant="Variant.Text" Color="Color.Default">بستن</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!;
/// <summary>Number of previous purchase cycles — controls Daya eligibility</summary>
[Parameter] public int PurchaseCycleCount { get; set; }
private List<PackageDto> _packages = new();
private PackageDto? _selectedPackage;
private bool _isLoading = true;
protected override async Task OnInitializedAsync()
{
_isLoading = true;
try
{
_packages = await PackageService.GetAllPackagesAsync();
}
catch (Exception ex)
{
Snackbar.Add("خطا در بارگذاری پکیج‌ها", MudBlazor.Severity.Error);
}
finally
{
_isLoading = false;
}
}
private void SelectPackage(PackageDto package)
{
_selectedPackage = package;
}
private void BackToList()
{
_selectedPackage = null;
}
private void SelectPaymentMethod(PaymentMethodType method)
{
var result = new PackagePurchaseResult(_selectedPackage!, method);
MudDialog.Close(DialogResult.Ok(result));
}
private void Cancel() => MudDialog.Cancel();
private static string GetTileStyle(PackageDto pkg)
{
return pkg.IsBasePackage
? "border:2px solid var(--mud-palette-primary); background:rgba(99,102,241,.04); cursor:pointer;"
: "border:2px solid var(--mud-palette-lines-default); background:var(--mud-palette-surface); cursor:pointer;";
}
private static string GetAvatarStyle(PackageDto pkg)
{
return pkg.IsBasePackage
? "background:rgba(99,102,241,.15); color:#6366f1;"
: "background:rgba(16,185,129,.12); color:#10b981;";
}
public enum PaymentMethodType { DirectPayment, DayaLoan }
public record PackagePurchaseResult(PackageDto Package, PaymentMethodType Method);
}
@@ -15,6 +15,8 @@ public class CommissionPayoutDto
public string AmountFormatted { get; set; } = string.Empty; public string AmountFormatted { get; set; } = string.Empty;
public int Status { get; set; } public int Status { get; set; }
public string DatePersian { get; set; } = string.Empty; public string DatePersian { get; set; } = string.Empty;
public long PackageId { get; set; }
public string PackageTitle { get; set; } = string.Empty;
// Computed properties for UI // Computed properties for UI
public string StatusText => Status switch public string StatusText => Status switch
@@ -69,6 +71,8 @@ public class WeeklyBalanceDto
public int RightNewMembers { get; set; } public int RightNewMembers { get; set; }
public DateTime StartDate { get; set; } public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; } public DateTime EndDate { get; set; }
public long PackageId { get; set; }
public string PackageTitle { get; set; } = string.Empty;
// Formatted properties // Formatted properties
public string LeftBalanceFormatted => $"{LeftBalance:N0} "; public string LeftBalanceFormatted => $"{LeftBalance:N0} ";
@@ -78,7 +78,8 @@ public class CommissionService
long? weekDefinitionId, long? weekDefinitionId,
string? status, string? status,
int pageNumber, int pageNumber,
int pageSize) int pageSize,
long? packageId = null)
{ {
try try
{ {
@@ -108,6 +109,11 @@ public class CommissionService
}; };
} }
if (packageId.HasValue && packageId.Value > 0)
{
request.PackageId = packageId.Value;
}
var response = await _client.GetMyCommissionPayoutsAsync(request); var response = await _client.GetMyCommissionPayoutsAsync(request);
return new CommissionPayoutsResponseDto return new CommissionPayoutsResponseDto
@@ -121,7 +127,9 @@ public class CommissionService
TotalAmount = p.TotalAmount, TotalAmount = p.TotalAmount,
AmountFormatted = p.AmountFormatted, AmountFormatted = p.AmountFormatted,
Status = p.Status, Status = p.Status,
DatePersian = p.DatePersian DatePersian = p.DatePersian,
PackageId = p.PackageId,
PackageTitle = p.PackageTitle
}).ToList(), }).ToList(),
TotalCount = (int)(response.MetaData?.TotalCount ?? 0), TotalCount = (int)(response.MetaData?.TotalCount ?? 0),
PageNumber = pageNumber, PageNumber = pageNumber,
@@ -145,7 +153,7 @@ public class CommissionService
/// Get weekly balance details for a specific week /// Get weekly balance details for a specific week
/// Maps to: CommissionCQ.GetMyWeeklyBalances /// Maps to: CommissionCQ.GetMyWeeklyBalances
/// </summary> /// </summary>
public async Task<WeeklyBalanceDto> GetMyWeeklyBalanceAsync(long? weekDefinitionId = null) public async Task<WeeklyBalanceDto> GetMyWeeklyBalanceAsync(long? weekDefinitionId = null, long? packageId = null)
{ {
try try
{ {
@@ -164,6 +172,11 @@ public class CommissionService
request.WeekDefinitionId=7; request.WeekDefinitionId=7;
} }
if (packageId.HasValue && packageId.Value > 0)
{
request.PackageId = packageId.Value;
}
var response = await _client.GetMyWeeklyBalancesAsync(request); var response = await _client.GetMyWeeklyBalancesAsync(request);
if (response.Balances.Count > 0) if (response.Balances.Count > 0)
@@ -183,7 +196,9 @@ public class CommissionService
LeftNewMembers = balance.LeftLegNewMembers, LeftNewMembers = balance.LeftLegNewMembers,
RightNewMembers = balance.RightLegNewMembers, RightNewMembers = balance.RightLegNewMembers,
StartDate = balance.CalculatedAt?.ToDateTime().AddDays(-7) ?? DateTime.Now.AddDays(-7), StartDate = balance.CalculatedAt?.ToDateTime().AddDays(-7) ?? DateTime.Now.AddDays(-7),
EndDate = balance.CalculatedAt?.ToDateTime() ?? DateTime.Now EndDate = balance.CalculatedAt?.ToDateTime() ?? DateTime.Now,
PackageId = balance.PackageId,
PackageTitle = balance.PackageTitle
}; };
} }
@@ -227,7 +242,9 @@ public class CommissionService
TotalAmount = p.TotalAmount, TotalAmount = p.TotalAmount,
AmountFormatted = p.AmountFormatted, AmountFormatted = p.AmountFormatted,
Status = p.Status, Status = p.Status,
DatePersian = p.DatePersian DatePersian = p.DatePersian,
PackageId = p.PackageId,
PackageTitle = p.PackageTitle
}).ToList(); }).ToList();
} }
catch catch
@@ -112,6 +112,27 @@ public class DiscountOrderService
catch { return false; } catch { return false; }
} }
public async Task<(bool Success, string Message, long OrderId)> VerifyDiscountOrderPaymentAsync(
long orderId, string authority, string status)
{
try
{
var response = await _client.CustomerVerifyDiscountOrderPaymentAsync(
new CustomerVerifyDiscountOrderPaymentRequest
{
OrderId = orderId,
Authority = authority,
Status = status
});
return (response.Success, response.Message, response.OrderId);
}
catch (Grpc.Core.RpcException ex)
{
return (false, ex.Status.Detail ?? "خطا در بررسی وضعیت پرداخت", orderId);
}
}
public async Task<DiscountOrderListResult> GetUserOrdersAsync(int page = 1, int pageSize = 10) public async Task<DiscountOrderListResult> GetUserOrdersAsync(int page = 1, int pageSize = 10)
{ {
try try
@@ -97,4 +97,22 @@ public class OrderService
return 0.10; return 0.10;
} }
} }
/// <summary>
/// محاسبه PV سفارش
/// </summary>
public async Task<CalculateOrderPVResponse?> CalculateOrderPVAsync(long orderId)
{
try
{
return await _userOrderContractClient.CalculateOrderPVAsync(new CalculateOrderPVRequest
{
OrderId = orderId
});
}
catch
{
return null;
}
}
} }
@@ -10,8 +10,17 @@ public record PackageDto(
string Title, string Title,
string Description, string Description,
string ImageUrl, string ImageUrl,
long Price) long Price,
long ActivationFee = 0,
double DiscountMultiplier = 0,
double MagicWalletMultiplier = 0,
long MagicWalletMaxDeposit = 0,
long MagicWalletMaxCredit = 0,
bool IsBasePackage = false,
bool SupportsDayaPurchase = false,
bool SupportsDirectPurchase = false)
{ {
/// <summary>قیمت فرمت‌شده به تومان</summary>
public string FormattedPrice => string.Format("{0:N0} تومان", Price); public string FormattedPrice => string.Format("{0:N0} تومان", Price);
} }
@@ -20,6 +29,7 @@ public record PackageDto(
/// </summary> /// </summary>
public record UserPackageStatusDto( public record UserPackageStatusDto(
bool HasPurchasedPackage, bool HasPurchasedPackage,
string? PackageTitle,
string? PurchaseMethod, // DayaLoan, DirectPurchase, or null string? PurchaseMethod, // DayaLoan, DirectPurchase, or null
bool IsClubMemberActive, bool IsClubMemberActive,
long WalletBalance, long WalletBalance,
@@ -55,7 +65,15 @@ public class PackageService
m.Title, m.Title,
m.Description, m.Description,
m.ImagePath, m.ImagePath,
m.Price)) m.Price,
m.ActivationFee,
m.DiscountMultiplier,
m.MagicWalletMultiplier,
m.MagicWalletMaxDeposit,
m.MagicWalletMaxCredit,
m.IsBasePackage,
m.SupportsDayaPurchase,
m.SupportsDirectPurchase))
.ToList(); .ToList();
} }
catch (Exception ex) catch (Exception ex)
@@ -99,15 +117,32 @@ public class PackageService
/// <summary> /// <summary>
/// Get user's package purchase status /// Get user's package purchase status
/// </summary> /// </summary>
public Task<UserPackageStatusDto> GetUserPackageStatusAsync(CancellationToken ct = default) public async Task<UserPackageStatusDto> GetUserPackageStatusAsync(CancellationToken ct = default)
{ {
// TODO: Connect to GetUserPackageStatus RPC when available in FrontOffice.BFF try
return Task.FromResult(new UserPackageStatusDto( {
HasPurchasedPackage: false, var response = await _client.GetUserPackageStatusAsync(
PurchaseMethod: null, new GetUserPackageStatusRequest(), cancellationToken: ct);
IsClubMemberActive: false,
WalletBalance: 0, return new UserPackageStatusDto(
CanActivateClubMembership: false, HasPurchasedPackage: response.HasPurchasedPackage,
PurchaseDate: null)); PackageTitle: null,
PurchaseMethod: response.PackagePurchaseMethod,
IsClubMemberActive: response.IsClubMemberActive,
WalletBalance: response.WalletBalance,
CanActivateClubMembership: response.CanActivateClubMembership,
PurchaseDate: response.LastPurchaseDate?.ToDateTime());
}
catch
{
return new UserPackageStatusDto(
HasPurchasedPackage: false,
PackageTitle: null,
PurchaseMethod: null,
IsClubMemberActive: false,
WalletBalance: 0,
CanActivateClubMembership: false,
PurchaseDate: null);
}
} }
} }
@@ -1,6 +1,5 @@
using DateTimeConverterCL; using DateTimeConverterCL;
using CMSMicroservice.Protobuf.Protos.UserWallet; using CMSMicroservice.Protobuf.Protos.UserWallet;
using CMSMicroservice.Protobuf.Protos.UserWalletChangeLog;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
namespace FrontOffice.Main.Utilities; namespace FrontOffice.Main.Utilities;
@@ -33,7 +32,9 @@ public record MagicWalletStatus(
long MagicRemainingDeposit, long MagicRemainingDeposit,
long Balance, long Balance,
DateTime? MagicActivatedAt, DateTime? MagicActivatedAt,
int PurchaseCycleCount int PurchaseCycleCount,
double MagicMultiplier = 2.5,
long MagicMaxCredit = 2_500_000_000
); );
public class WalletService public class WalletService
@@ -63,7 +64,7 @@ public class WalletService
{ {
try try
{ {
var request = new GetCustomerWalletChangeLogRequest(); var request = new GetCustomerWalletHistoryRequest();
if (referenceId.HasValue) if (referenceId.HasValue)
{ {
request.ReferenceId = referenceId.Value; request.ReferenceId = referenceId.Value;
@@ -72,7 +73,7 @@ public class WalletService
{ {
request.IsIncrease = isIncrease.Value; request.IsIncrease = isIncrease.Value;
} }
var response = await _client.GetCustomerWalletChangeLogAsync(request); var response = await _client.GetCustomerWalletHistoryAsync(request);
return response.Models return response.Models
.Select(log => new WalletTransaction( .Select(log => new WalletTransaction(
@@ -96,7 +97,7 @@ public class WalletService
} }
} }
private static string ResolveTransactionDate(CustomerWalletChangeLogModel model) private static string ResolveTransactionDate(CustomerWalletHistoryModel model)
{ {
if (model.CreatedAt is not null) if (model.CreatedAt is not null)
{ {
@@ -114,7 +115,7 @@ public class WalletService
return DateTime.Now.MiladiToJalaliWithTime(); return DateTime.Now.MiladiToJalaliWithTime();
} }
private static string ResolveDescription(CustomerWalletChangeLogModel model) private static string ResolveDescription(CustomerWalletHistoryModel model)
{ {
var parts = new List<string>(); var parts = new List<string>();
@@ -214,7 +215,9 @@ public class WalletService
response.MagicRemainingDeposit, response.MagicRemainingDeposit,
response.Balance, response.Balance,
activatedAt, activatedAt,
response.PurchaseCycleCount response.PurchaseCycleCount,
response.MagicMultiplier > 0 ? response.MagicMultiplier : 2.5,
response.MagicMaxCredit > 0 ? response.MagicMaxCredit : 2_500_000_000
); );
} }
catch catch
@@ -263,6 +266,44 @@ public class WalletService
} }
} }
// ============= Wallet Verify Methods =============
public async Task<(bool Success, string Message)> VerifyMagicChargeAsync(string authority, string status)
{
try
{
var response = await _client.VerifyMagicChargeAsync(new VerifyWalletChargeRequest
{
Authority = authority,
Status = status
});
return (response.Success, response.Message);
}
catch (Grpc.Core.RpcException ex)
{
return (false, ex.Status.Detail ?? "خطا در بررسی وضعیت پرداخت");
}
}
public async Task<(bool Success, string Message)> VerifyDiscountChargeAsync(string authority, string status)
{
try
{
var response = await _client.VerifyDiscountChargeAsync(new VerifyWalletChargeRequest
{
Authority = authority,
Status = status
});
return (response.Success, response.Message);
}
catch (Grpc.Core.RpcException ex)
{
return (false, ex.Status.Detail ?? "خطا در بررسی وضعیت پرداخت");
}
}
public int GetWalletMode() public int GetWalletMode()
{ {
try try
+27
View File
@@ -1611,3 +1611,30 @@ html, body {
} }
/*#endregion*/ /*#endregion*/
/* ── Package Purchase Dialog tiles ── */
.pkg-tile {
transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease;
position: relative;
}
.pkg-tile:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0,0,0,.08);
border-color: var(--mud-palette-primary) !important;
}
.pkg-tile-badge {
position: absolute;
top: 8px;
right: 8px;
}
.pkg-payment-option {
transition: transform .15s ease, box-shadow .15s ease;
}
.pkg-payment-option:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,.06);
}
@media (max-width: 600px) {
.pkg-tile { padding: .8rem !important; }
.pkg-tile .mud-avatar-large { width: 48px !important; height: 48px !important; }
}