Compare commits

...

7 Commits

Author SHA1 Message Date
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
20 changed files with 442 additions and 133 deletions
+1 -7
View File
@@ -11,7 +11,7 @@
<ItemGroup>
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
<!-- 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.188" />
<!-- <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />-->
<PackageReference Include="MudBlazor" Version="8.14.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
@@ -56,12 +56,6 @@
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
<Content Remove="Pages\Package\Packages.razor" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Pages\Package\Packages.razor.cs" />
</ItemGroup>
</Project>
+2
View File
@@ -2,6 +2,8 @@
<configuration>
<packageSources>
<clear />
<!-- Local packages (dev builds) -->
<add key="Local" value="/home/masoud/Apps/project/FourSat/nupkg" />
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
</packageSources>
+22 -1
View File
@@ -207,11 +207,32 @@
FullWidth="true"
StartIcon="@Icons.Material.Filled.Payment"
OnClick="ProcessPayment"
Disabled="@(!CanProceedToPayment || _isProcessingPayment)"
Disabled="@(!CanProceedToPayment || _isProcessingPayment || !_selectedPackage.SupportsDirectPurchase)"
Class="mt-2">
@(_isProcessingPayment ? "در حال پردازش..." : "پرداخت آنلاین")
</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)
{
<MudText Typo="Typo.caption" Color="Color.Error" Align="Align.Center">
+44 -39
View File
@@ -1,11 +1,9 @@
using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.Transactions;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using FrontOffice.Main.Utilities;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
using Severity = MudBlazor.Severity;
@@ -13,10 +11,9 @@ namespace FrontOffice.Main.Pages;
public partial class Checkout
{
[Inject] private PackageService PackageService { get; set; } = default!;
[Inject] private PackageContract.PackageContractClient PackageClient { 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; }
@@ -52,15 +49,18 @@ public partial class Checkout
{
try
{
var response = await PackageClient.GetPackageAsync(new() { Id = PackageId.Value });
if (response != null)
var packages = await PackageService.GetAllPackagesAsync();
var pkg = packages.FirstOrDefault(p => p.Id == PackageId.Value);
if (pkg != null)
{
_selectedPackage = new Pack(
Id: response.Id,
Title: response.Title,
Body: response.Description,
Image: response.ImagePath ?? string.Empty,
Price: response.Price
Id: pkg.Id,
Title: pkg.Title,
Body: pkg.Description,
Image: pkg.ImageUrl,
Price: pkg.Price,
SupportsDirectPurchase: pkg.SupportsDirectPurchase,
SupportsDayaPurchase: pkg.SupportsDayaPurchase
);
_finalPrice = _selectedPackage.Price;
}
@@ -173,9 +173,6 @@ public partial class Checkout
private async Task ProcessPayment()
{
Snackbar.Add("درگاه پرداخت متصل نیست! لطفا در زمان دیگری مجددا تلاش فرمایید!", Severity.Warning);
return;
if (!CanProceedToPayment || _selectedPackage == null || _selectedAddress == null)
{
Snackbar.Add("لطفاً پکیج و آدرس را انتخاب کنید.", Severity.Warning);
@@ -186,33 +183,29 @@ public partial class Checkout
try
{
// Step 1: Create payment request
var paymentRequest = new CustomerPaymentRequestRequest
var callbackUrl = $"{Navigation.BaseUri}profile/payment-callback";
var response = await PackageClient.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
{
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,
PaymentStatus = CMSMicroservice.Protobuf.Protos.PaymentStatus.Pending
};
CallbackUrl = callbackUrl,
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
Navigation.NavigateTo(paymentResponse.PaymentGWUrl);
if (!string.IsNullOrEmpty(response.PaymentGatewayUrl))
{
Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true);
}
else
{
Snackbar.Add("خطا در دریافت آدرس درگاه پرداخت", Severity.Error);
}
}
catch (Exception ex)
{
@@ -225,5 +218,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);
}
@@ -13,7 +13,7 @@
<!-- فیلترها -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
<MudGrid Spacing="2">
<MudItem xs="12" sm="6" md="4">
<MudItem xs="12" sm="6" md="3">
<WeekSelector @ref="_weekSelector"
@bind-Value="_selectedWeekDefinition"
Label="انتخاب هفته"
@@ -23,7 +23,7 @@
Clearable="true"
OnlyActive="false" />
</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">
<MudSelectItem T="string" Value="@string.Empty">همه</MudSelectItem>
<MudSelectItem T="string" Value="@("Pending")">در انتظار</MudSelectItem>
@@ -34,7 +34,16 @@
<MudSelectItem T="string" Value="@("Cancelled")">لغو شده</MudSelectItem>
</MudSelect>
</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">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ApplyFiltersAsync" StartIcon="@Icons.Material.Filled.FilterList" FullWidth="true">
اعمال فیلتر
@@ -87,6 +96,7 @@
<MudTable Items="_payouts" Hover="true" Striped="true" Dense="true" FixedHeader="true" Height="400px">
<HeaderContent>
<MudTh>هفته</MudTh>
<MudTh>پکیج</MudTh>
<MudTh>امتیاز</MudTh>
<MudTh>مبلغ (تومان)</MudTh>
<MudTh>وضعیت</MudTh>
@@ -97,6 +107,11 @@
<MudTd DataLabel="هفته">
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@context.WeekDisplayName</MudChip>
</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="مبلغ">
<MudText Color="Color.Success"><strong>@context.AmountFormatted</strong></MudText>
@@ -127,6 +142,10 @@
<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>
</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">
<MudText Typo="Typo.h6" Color="Color.Success">@payout.AmountFormatted</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
{
[Inject] private CommissionService CommissionService { get; set; } = default!;
[Inject] private PackageService PackageService { get; set; } = default!;
private List<CommissionPayoutDto> _payouts = new();
private List<PackageDto> _packages = new();
private int _totalCount;
private long _totalAmount;
private int _pageNumber = 1;
@@ -19,12 +21,26 @@ public partial class CommissionDashboardPage : ComponentBase
private WeekSelector? _weekSelector;
private WeekDefinitionDto? _selectedWeekDefinition;
private string _filterStatus = string.Empty;
private long? _filterPackageId;
protected override async Task OnInitializedAsync()
{
await LoadPackagesAsync();
await LoadPayoutsAsync();
}
private async Task LoadPackagesAsync()
{
try
{
_packages = await PackageService.GetAllPackagesAsync();
}
catch
{
_packages = new List<PackageDto>();
}
}
private async Task LoadPayoutsAsync()
{
try
@@ -35,7 +51,8 @@ public partial class CommissionDashboardPage : ComponentBase
weekDefinitionId,
_filterStatus,
_pageNumber,
_pageSize
_pageSize,
_filterPackageId
);
_payouts = result.Payouts;
_totalCount = result.TotalCount;
@@ -61,6 +78,7 @@ public partial class CommissionDashboardPage : ComponentBase
{
_selectedWeekDefinition = null;
_filterStatus = string.Empty;
_filterPackageId = null;
_pageNumber = 1;
await LoadPayoutsAsync();
}
@@ -12,7 +12,7 @@
<!-- انتخاب هفته -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
<MudGrid Spacing="2">
<MudItem xs="8" sm="6" md="6">
<MudItem xs="8" sm="4" md="4">
<WeekSelector @ref="_weekSelector"
@bind-Value="_selectedWeekDefinition"
Label="انتخاب هفته"
@@ -21,15 +21,15 @@
Dense="false"
OnlyActive="true" />
</MudItem>
@* <MudItem xs="4" sm="3" md="3"> *@
@* <MudButton Variant="Variant.Outlined" *@
@* OnClick="LoadCurrentWeekAsync" *@
@* StartIcon="@Icons.Material.Filled.Today" *@
@* FullWidth="true" *@
@* Style="height: 56px;"> *@
@* هفته جاری *@
@* </MudButton> *@
@* </MudItem> *@
<MudItem xs="4" sm="4" md="4">
<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="4" sm="3" md="3" Class="pt-6">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
@@ -66,6 +66,12 @@
<MudItem xs="6" sm="4">
<MudText Typo="Typo.caption" Class="mud-text-secondary">پایان: <strong>@_weeklyBalance.EndDatePersian</strong></MudText>
</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>
</MudPaper>
@@ -8,6 +8,7 @@ namespace FrontOffice.Main.Pages.Commission;
public partial class WeeklyBalancePage : ComponentBase
{
[Inject] private CommissionService CommissionService { get; set; } = default!;
[Inject] private PackageService PackageService { get; set; } = default!;
[Inject] private ISnackbar SnackbarService { get; set; } = default!;
[Parameter]
@@ -17,6 +18,8 @@ public partial class WeeklyBalancePage : ComponentBase
private WeekSelector? _weekSelector;
private WeekDefinitionDto? _selectedWeekDefinition;
private WeeklyBalanceDto? _weeklyBalance;
private List<PackageDto> _packages = new();
private long? _filterPackageId;
private bool _isLoading = true;
private bool _hasError = false;
@@ -34,6 +37,8 @@ public partial class WeeklyBalancePage : ComponentBase
{
if (firstRender)
{
await LoadPackagesAsync();
// Wait for WeekSelector to initialize and load its cached weeks
if (_weekSelector != null)
{
@@ -88,7 +93,7 @@ public partial class WeeklyBalancePage : ComponentBase
long? weekDefinitionId = _selectedWeekDefinition?.Id;
_weeklyBalance = await CommissionService.GetMyWeeklyBalanceAsync(weekDefinitionId);
_weeklyBalance = await CommissionService.GetMyWeeklyBalanceAsync(weekDefinitionId, _filterPackageId);
// Update week display name from selected definition if available
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()
{
if (_weekSelector != null)
@@ -20,7 +20,7 @@
<MudIcon Icon="@Icons.Material.Filled.CardGiftcard" Size="Size.Large" Color="Color.Primary" Style="font-size: 80px;" />
<MudText Typo="Typo.h5">شما هنوز پکیجی خریداری نکرده‌اید</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width: 500px;">
با خرید پکیج طلایی، به باشگاه مشتریان بپیوندید و از مزایای ویژه مانند پاداش هفتگی و تیم‌سازی بهره‌مند شوید.
با خرید پکیج، به باشگاه مشتریان بپیوندید و از مزایای ویژه مانند پاداش هفتگی و تیم‌سازی بهره‌مند شوید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
@@ -35,6 +35,70 @@
else
{
<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 -->
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-6 h-100">
@@ -49,7 +113,7 @@
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">نوع پکیج:</MudText>
<MudText Typo="Typo.body2">پکیج طلایی</MudText>
<MudText Typo="Typo.body2">@_userStatus.PackageTitle</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
@@ -146,7 +210,7 @@
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Stars" Color="Color.Warning" />
<MudText Typo="Typo.h6">مزایای پکیج طلایی</MudText>
<MudText Typo="Typo.h6">مزایای پکیج</MudText>
</MudStack>
<MudDivider />
@@ -9,7 +9,9 @@ public partial class MyPackages : ComponentBase
[Inject] private PackageService PackageService { get; set; } = default!;
private UserPackageStatusDto? _userStatus;
private MagicWalletStatus? _magicStatus;
private bool _isLoading = true;
private bool _canRepurchase = false;
private List<BreadcrumbItem> _breadcrumbItems = new()
{
@@ -30,6 +32,18 @@ public partial class MyPackages : ComponentBase
try
{
_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)
{
@@ -67,4 +81,11 @@ public partial class MyPackages : ComponentBase
{
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);
}
}
@@ -27,7 +27,7 @@
{
<MudAlert Severity="Severity.Success" Class="mb-6" Icon="@Icons.Material.Filled.CheckCircle">
<MudText>
شما قبلاً پکیج طلایی را خریداری کرده‌اید.
شما قبلاً پکیج را خریداری کرده‌اید.
@if (_userStatus.IsClubMemberActive)
{
<span>عضویت باشگاه شما فعال است.</span>
@@ -64,20 +64,43 @@
<MudText Typo="Typo.h6" Color="Color.Success">@package.FormattedPrice</MudText>
</MudStack>
<!-- Features Preview -->
<!-- Package Highlights -->
<MudStack Spacing="1" Class="mt-2">
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">عضویت در باشگاه مشتریان</MudText>
</MudStack>
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">دریافت پاداش هفتگی</MudText>
</MudStack>
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">تیم‌سازی نامحدود</MudText>
</MudStack>
@if (package.SupportsDirectPurchase)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<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" />
<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>
</MudCardContent>
@@ -100,9 +123,9 @@
<MudGrid Spacing="4">
<MudItem xs="12" md="8">
<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>
</MudStack>
@@ -64,36 +64,38 @@ public partial class PackageDetail : IDisposable
try
{
// Load package details
var packageResponse = await PackageClient.GetPackageAsync(request: new() { Id = Id}, cancellationToken: _loadCts.Token);
// Load package details via Customer RPC
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
{
Id = packageResponse.Id,
Title = packageResponse.Title,
Body = packageResponse.Description,
Image = packageResponse.ImagePath ?? string.Empty,
Specifications = new List<Specification>
{
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>
{
"مدیریت تیم نامحدود",
"شجره‌نامه بصری",
"محاسبه کارمزد خودکار",
"گزارش‌های مالی",
"پشتیبانی اولویت‌دار"
},
Id = response.Id,
Title = response.Title,
Body = response.Description,
Image = response.ImagePath ?? string.Empty,
Specifications = specs,
Features = features,
Pricing = new PricingInfo
{
OriginalPrice = packageResponse.Price,
FinalPrice = packageResponse.Price,
OriginalPrice = response.Price,
FinalPrice = response.Price,
HasDiscount = false,
DiscountPercent = 0
}
@@ -413,13 +413,23 @@ public partial class Index
try
{
// Create callback URL for payment verification
// Get available packages and pick the first one
var packages = await PackageContract.GetCustomerPackagesAsync(new GetCustomerPackagesRequest());
var selectedPackage = packages?.Models?.FirstOrDefault();
if (selectedPackage == null)
{
Snackbar.Add("پکیجی برای خرید یافت نشد", Severity.Error);
return;
}
// Call CMS to initiate purchase (UserId from JWT, PackageId explicit)
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
var response = await PackageContract.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
{
CallbackUrl = callbackUrl
PackageId = selectedPackage.Id,
CallbackUrl = callbackUrl,
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
});
if (!response.Success)
@@ -429,6 +439,8 @@ public partial class Index
}
// Redirect to payment gateway
// ZarinPal appends ?Authority=...&Status=... to callback URL
// PaymentCallback uses Authority+Status to verify via CustomerVerifyPackagePurchase
if (!string.IsNullOrEmpty(response.PaymentGatewayUrl))
{
Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true);
@@ -101,7 +101,7 @@
{
try
{
if (OrderId <= 0 || TransactionId <= 0 || string.IsNullOrEmpty(Authority))
if (OrderId <= 0 || string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "پارامترهای پرداخت نامعتبر است";
@@ -110,20 +110,23 @@
return;
}
var response = await PackageContractClient.VerifyBasePackagePaymentAsync(new VerifyBasePackagePaymentRequest
var response = await PackageContractClient.CustomerVerifyPackagePurchaseAsync(new CustomerVerifyPackagePurchaseRequest
{
OrderId = OrderId,
TransactionId = TransactionId,
PaymentSuccess = Status == "OK",
RefId = !string.IsNullOrEmpty(Authority) ? Authority : null,
Message = !string.IsNullOrEmpty(Status) ? Status : null
Authority = Authority ?? string.Empty,
Status = Status ?? string.Empty
});
_isSuccess = response.Success;
_message = response.Message;
_refId = response.ReferenceCode ?? "";
_walletBalance = response.WalletBalance;
_discountBalance = response.DiscountBalance;
// Wallet balances are no longer in verify response — show from purchase info
if (response.PurchaseInfo != null)
{
_walletBalance = response.PurchaseInfo.AmountPaid;
_discountBalance = 0; // Available in wallet page
}
// اگر پرداخت موفق بود، توکن را refresh می‌کنیم
// چون PackagePurchaseMethod تغییر کرده و باید در claims جدید باشد
@@ -112,6 +112,30 @@ else
</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>
</MudPaper>
</MudContainer>
@@ -12,12 +12,17 @@ public partial class OrderDetail : ComponentBase
[Parameter] public long id { get; set; }
private GetUserOrderResponse? _order;
private CalculateOrderPVResponse? _pvData;
private bool _loading;
protected override async Task OnParametersSetAsync()
{
_loading = true;
_order = await OrderService.GetOrderAsync(id);
if (_order != null)
{
_pvData = await OrderService.CalculateOrderPVAsync(id);
}
_loading = false;
}
@@ -15,6 +15,8 @@ public class CommissionPayoutDto
public string AmountFormatted { get; set; } = string.Empty;
public int Status { get; set; }
public string DatePersian { get; set; } = string.Empty;
public long PackageId { get; set; }
public string PackageTitle { get; set; } = string.Empty;
// Computed properties for UI
public string StatusText => Status switch
@@ -69,6 +71,8 @@ public class WeeklyBalanceDto
public int RightNewMembers { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public long PackageId { get; set; }
public string PackageTitle { get; set; } = string.Empty;
// Formatted properties
public string LeftBalanceFormatted => $"{LeftBalance:N0} ";
@@ -78,7 +78,8 @@ public class CommissionService
long? weekDefinitionId,
string? status,
int pageNumber,
int pageSize)
int pageSize,
long? packageId = null)
{
try
{
@@ -108,6 +109,11 @@ public class CommissionService
};
}
if (packageId.HasValue && packageId.Value > 0)
{
request.PackageId = packageId.Value;
}
var response = await _client.GetMyCommissionPayoutsAsync(request);
return new CommissionPayoutsResponseDto
@@ -121,7 +127,9 @@ public class CommissionService
TotalAmount = p.TotalAmount,
AmountFormatted = p.AmountFormatted,
Status = p.Status,
DatePersian = p.DatePersian
DatePersian = p.DatePersian,
PackageId = p.PackageId,
PackageTitle = p.PackageTitle
}).ToList(),
TotalCount = (int)(response.MetaData?.TotalCount ?? 0),
PageNumber = pageNumber,
@@ -145,7 +153,7 @@ public class CommissionService
/// Get weekly balance details for a specific week
/// Maps to: CommissionCQ.GetMyWeeklyBalances
/// </summary>
public async Task<WeeklyBalanceDto> GetMyWeeklyBalanceAsync(long? weekDefinitionId = null)
public async Task<WeeklyBalanceDto> GetMyWeeklyBalanceAsync(long? weekDefinitionId = null, long? packageId = null)
{
try
{
@@ -164,6 +172,11 @@ public class CommissionService
request.WeekDefinitionId=7;
}
if (packageId.HasValue && packageId.Value > 0)
{
request.PackageId = packageId.Value;
}
var response = await _client.GetMyWeeklyBalancesAsync(request);
if (response.Balances.Count > 0)
@@ -183,7 +196,9 @@ public class CommissionService
LeftNewMembers = balance.LeftLegNewMembers,
RightNewMembers = balance.RightLegNewMembers,
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,
AmountFormatted = p.AmountFormatted,
Status = p.Status,
DatePersian = p.DatePersian
DatePersian = p.DatePersian,
PackageId = p.PackageId,
PackageTitle = p.PackageTitle
}).ToList();
}
catch
@@ -97,4 +97,22 @@ public class OrderService
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,7 +10,15 @@ public record PackageDto(
string Title,
string Description,
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)
{
public string FormattedPrice => string.Format("{0:N0} تومان", Price);
}
@@ -20,6 +28,7 @@ public record PackageDto(
/// </summary>
public record UserPackageStatusDto(
bool HasPurchasedPackage,
string? PackageTitle,
string? PurchaseMethod, // DayaLoan, DirectPurchase, or null
bool IsClubMemberActive,
long WalletBalance,
@@ -55,7 +64,15 @@ public class PackageService
m.Title,
m.Description,
m.ImagePath,
m.Price))
m.Price,
m.ActivationFee,
m.DiscountMultiplier,
m.MagicWalletMultiplier,
m.MagicWalletMaxDeposit,
m.MagicWalletMaxCredit,
m.IsBasePackage,
m.SupportsDayaPurchase,
m.SupportsDirectPurchase))
.ToList();
}
catch (Exception ex)
@@ -99,15 +116,32 @@ public class PackageService
/// <summary>
/// Get user's package purchase status
/// </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
return Task.FromResult(new UserPackageStatusDto(
HasPurchasedPackage: false,
PurchaseMethod: null,
IsClubMemberActive: false,
WalletBalance: 0,
CanActivateClubMembership: false,
PurchaseDate: null));
try
{
var response = await _client.GetUserPackageStatusAsync(
new GetUserPackageStatusRequest(), cancellationToken: ct);
return new UserPackageStatusDto(
HasPurchasedPackage: response.HasPurchasedPackage,
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);
}
}
}