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)
This commit is contained in:
masoodafar-web
2026-02-26 17:52:37 +03:30
parent 0bbc11e82c
commit d71d463668
5 changed files with 109 additions and 56 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.185" />
<!-- <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>
@@ -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>
@@ -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
}
@@ -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);
}
@@ -56,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)
@@ -100,16 +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,
PackageTitle: null,
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);
}
}
}