18 Commits

Author SHA1 Message Date
masoodafar-web dcd11351a3 feat(T4.13): add PackageFeature CRUD support
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m41s
- Proto: add feature_ids to Create/Update/Get package messages
- Commands: add FeatureIds to Create/Update commands
- Handlers: sync PackageFeature join entities on create/update
- Queries: include PackageFeatures, map FeatureIds in responses
- Bump NuGet to v0.0.188
2026-02-26 21:59:24 +03:30
masoodafar-web aaaf7fc1ca feat(commission): add per-package filtering to commission & balance queries
- Proto: add package_id filter to 4 request messages, package_id/package_title to 4 response models
- Queries: add PackageId filter to GetUserWeeklyBalances, GetUserCommissionPayouts, GetMyCommissionPayouts, GetMyWeeklyBalances
- Handlers: include Package navigation, filter by PackageId, map PackageId/PackageTitle
- DTOs: add PackageId + PackageTitle to all response models
- Mappings: update CommissionProfile with PackageId/PackageTitle for all admin+customer mappings
- NuGet: bump proto version to 0.0.187
2026-02-26 21:07:59 +03:30
masoodafar-web 7554d7054a Phase 8d: Proto cleanup — delete 4 deprecated RPCs + 8 deprecated messages
- Deleted RPCs: PurchaseGoldenPackage, VerifyGoldenPackagePurchase,
  InitiateBasePackagePayment, VerifyBasePackagePayment
- Deleted messages: PurchaseGoldenPackage{Request,Response},
  VerifyGoldenPackagePurchase{Request,Response},
  InitiateBasePackagePayment{Request,Response},
  VerifyBasePackagePayment{Request,Response}
- Kept: GetUserPackageStatus (still in use)
- NuGet version bump: 0.0.185 → 0.0.186
2026-02-26 18:00:40 +03:30
masoodafar-web ce8e248843 Phase 8b: Bump Protobuf NuGet to v0.0.185 2026-02-26 17:42:59 +03:30
masoodafar-web 8446e0e3b5 Phase 7c: Delete deprecated handlers + clean service/mapping refs
- Delete 4 deprecated CQRS handler folders (12 files):
  - PurchaseGoldenPackage/ (Command, Handler, Validator)
  - VerifyGoldenPackagePurchase/ (Command, Handler, Validator)
  - InitiateBasePackagePayment/ (Command, Handler, Validator)
  - VerifyBasePackagePayment/ (Command, Handler, Validator)
- PackageService.cs: Remove 4 gRPC override methods that dispatched
  to deleted handlers (proto RPCs now auto-throw Unimplemented)
- PackageProfile.cs: Remove all Mapster mappings for deleted types
  (6 mapping blocks + 4 using directives removed)
- Customer* RPCs remain fully functional
- Build: 0 errors
2026-02-26 03:02:17 +03:30
masoodafar-web 161f796cd4 Phase 7b: Embed orderId in callback URL for CustomerPurchasePackage
- CustomerPurchasePackage: append orderId to callback URL before sending to gateway
- Ensures PaymentCallback page receives orderId via query string for verification
2026-02-26 02:34:47 +03:30
masoodafar-web 469d97bb60 Phase 7a: Cosmetic cleanup + delete orphaned PurchasePackage handler
- Delete orphaned PurchasePackageCommand handler (no RPC dispatch, no callers)
- Fix doc-comments: طلایی → پکیج in enums, entities, handlers
- Remove stale 56M inline comments
- Clean SystemConstants deprecated field docs
2026-02-26 02:01:33 +03:30
masoodafar-web d19c569aae Phase 6: Deprecation cleanup + ConfigurationService MagicWallet
- Mark PurchaseGoldenPackage/VerifyGoldenPackagePurchase RPCs as deprecated
- Mark InitiateBasePackagePayment/VerifyBasePackagePayment RPCs as deprecated
- Remove deprecated SystemConstants from GetAllAsDict/GetAllWithDescriptions helpers
- Add MagicWallet per-package values to ConfigurationService (Multiplier, MaxDeposit, MaxCredit)
- All deprecated constants have zero active code usages — safe dead code
2026-02-26 01:10:43 +03:30
masoodafar-web 7176fe44ee Fix SP: cm.PackageId → cm.LastPackageId (match actual DB column name) 2026-02-26 00:59:59 +03:30
masoodafar-web 607f791b65 Phase 5: Per-package commission + golden ref cleanup
- Fix golden/طلایی string refs in user-facing messages (ActivateClubMembership)
- Rename HasPurchasedGoldenPackage → HasPurchasedPackage (DTO, Handler, Proto, Mapping)
- ORM commission: per-user-package calculation via ClubMembership.LastPackageId
  - Build userPackageMap, per-user maxBalancesPerLeg/maxNetworkLevel
  - Carryover keyed by (UserId, PackageId) tuple
- SP commission: loop over packages, pass @PackageId/@InputMaxBalancesPerLeg/@InputMaxNetworkLevel
- sp_CalculateWeeklyBalances: accept 3 new params, filter by PackageId, insert PackageId column
2026-02-26 00:57:20 +03:30
masoodafar-web 0002a5a6f2 Phase 4: Complete Package CRUD DTOs + Legacy Fixes
- CreateNewPackageCommand: Add 12 new Package fields (SortOrder, IsActive, IsBasePackage,
  SupportsDayaPurchase, SupportsDirectPurchase, ActivationFee, DiscountMultiplier,
  MagicWalletMultiplier, MaxBalancesPerLeg, MaxNetworkLevel, MagicWalletMaxDeposit,
  MagicWalletMaxCredit) with sensible defaults
- GetPackageResponseDto: Add 12 new fields (Mapster auto-maps via ProjectToType)
- GetAllPackageByFilterResponseModel: Add 12 new fields (Mapster auto-maps)
- PurchaseGoldenPackage: Replace fragile Title string match ('طلایی'/'golden')
  with proper IsDeleted/IsActive/SupportsDirectPurchase validation
- VerifyPackagePurchase: Replace hardcoded order.Amount*2 with
  package.DiscountMultiplier from DB (fallback 2.0 for null Package)
2026-02-26 00:24:35 +03:30
masoodafar-web ccb938e9ba Phase 3: Package layer overhaul — fix critical bugs + proto enhancement
Proto (package.proto):
- Add 11 new Package fields to Create/Update/Get/GetAll messages
- Add 8 new fields to CustomerPackageModel for frontend

Critical Bug Fixes:
- VerifyGoldenPackagePurchase: Replace hardcoded ×2 with package.DiscountMultiplier from DB

Query Fixes:
- GetAllPackageByFilter: Add IsDeleted filter (was returning soft-deleted packages)
- GetCustomerPackages: Apply IsDeleted+IncludeInactive filters, sort by SortOrder, return new fields
- GetCustomerPackageDetails: Load PackageFeatures from DB (was hardcoded), add IsDeleted filter, return new fields
- GetCustomerPurchaseHistory: Include Transaction (was null → ReferenceCode always empty)

UpdatePackage: Add 12 new fields (SortOrder, IsActive, IsBasePackage, etc.)

Build: 0 errors
2026-02-25 23:50:09 +03:30
masoodafar-web 8e5c7c5205 Phase 2: Replace all deprecated SystemConstants with Package entity reads
- ActivateClubMembership: Load Package, set First/Last fields, PackageId on Cycle+Pool
- AcceptClubMembershipContract: Load Package, set First/Last fields, PackageId on Pool
- CalculateWeeklyBalances: Load base package, use package.MaxBalancesPerLeg/MaxNetworkLevel, set PackageId on NetworkWeeklyBalance
- ProcessUserPayouts: Load base package, use package.MaxNetworkLevel, set PackageId on UserCommissionPayout
- CheckAndProcessDayaLoans: Load base package (IsBasePackage), replace DayaLoanAmount→package.Price, fix hardcoded p.Id==4
- CreateManualPayment: Load base package, replace BasePackageAmount→package.Price, use DiscountMultiplier
- InitiateBasePackagePayment: Remove hardcoded BasePackageId=4, load from IsBasePackage, use package.Price/Id
- VerifyBasePackagePayment: Load Package from order.PackageId, use package.Price+DiscountMultiplier
- ChargeMagicWallet: Load user's package from current cycle, use package.MagicWalletMaxDeposit
- VerifyMagicWalletCharge: Load user's package, use package.MagicWalletMultiplier/MagicWalletMaxDeposit
- OrmCommissionCalculationStrategy: Load base package in both methods, set PackageId on Balance+Payout
- ConfigurationService: Inject IApplicationDbContext, read package values from DB instead of SystemConstants
- UserOrderService: Load user's package for EXIT trigger MagicWalletMaxDeposit check
- UserWalletService: Load user's package for GetMagicWalletStatus display

All 9 deprecated SystemConstants now have ZERO usages outside SystemConstants.cs
Build: 0 errors, 521 warnings (all pre-existing)
2026-02-25 23:25:18 +03:30
masoodafar-web a9cd2fd67a Phase 1.5: EF Migration + Data Seed + Backfill
MIGRATION (AddPackageBasedSystem):
- Adds 11 new columns to Packages table
- Creates PackageFeatures table (Package×ClubFeature junction)
- Adds PackageId FK to: ClubMembershipCycles, WeeklyCommissionPools,
  UserCommissionPayouts, NetworkWeeklyBalances, UserWalletChangeLogs
- Adds First/LastActivationDate, First/LastPackageId to ClubMemberships
- Drops old unique indexes, creates new composite unique indexes:
  (WeekDef,Package), (User,WeekDef,Package)

DATA MIGRATION (embedded in EF migration):
1. Seeds Golden Package (Id=1): Price=56M, ActivationFee=25.2M,
   DiscountMultiplier=2.0, MagicWalletMultiplier=2.5, MaxBalancesPerLeg=300,
   MaxNetworkLevel=15, MagicWalletMaxDeposit=1B, MagicWalletMaxCredit=2.5B
2. Backfills PackageId=1 on all existing commission/network records
3. Backfills ClubMembership First/Last fields from existing ActivatedAt

FIXES:
- ClubMembership First/Last fields now nullable (DateTime?, long?)
  for backward compat with existing records
- UserPackagePurchaseConfiguration: .WithMany() → .WithMany(p => p.Purchases)
  to prevent shadow FK PackageId1

Build: 0 errors, 73 warnings
2026-02-25 22:49:12 +03:30
masoodafar-web ae92ab8697 Phase 1: Package-Based Domain Infrastructure
ENTITIES:
- Package: +11 fields (SortOrder, IsActive, IsBasePackage, SupportsDaya/Direct,
  ActivationFee, DiscountMultiplier, MagicWalletMultiplier, MaxBalancesPerLeg,
  MaxNetworkLevel, MagicWalletMaxDeposit/Credit) + nav props
- PackageFeature: NEW junction entity (Package × ClubFeature) for feature DIFF
- ClubMembership: +4 fields (FirstActivationDate/PackageId, LastActivation/PackageId)
  ActivatedAt kept as [Obsolete] for backward compat
- ClubMembershipCycle: +PackageId FK (tracks which package per cycle)
- WeeklyCommissionPool: +PackageId FK (separate pool per package)
- UserCommissionPayout: +PackageId FK (payout per package)
- NetworkWeeklyBalance: +PackageId FK (balance per package)
- UserWalletChangeLog: +PackageId? FK (optional package tracking)

EF CONFIGS:
- PackageConfiguration: 11 new field configs + indexes
- PackageFeatureConfiguration: NEW (unique PackageId+ClubFeatureId)
- ClubMembershipConfiguration: First/Last Package FKs + LastActivationDate index
- ClubMembershipCycleConfiguration: Package FK
- WeeklyCommissionPoolConfiguration: Package FK + unique(WeekDef,Package)
- UserCommissionPayoutConfiguration: Package FK + unique(User,WeekDef,Package)
- NetworkWeeklyBalanceConfiguration: Package FK + unique(User,WeekDef,Package)
- UserWalletChangeLogConfiguration: Package FK (nullable)

DBCONTEXT:
- DbSet<PackageFeature> added to ApplicationDbContext + IApplicationDbContext

SYSTEMCONSTANTS:
- 9 constants marked [Obsolete] → moved to Package entity
  (ClubMembershipGiftValue, ClubActivationFee, BasePackageAmount, DayaLoanAmount,
   MagicWalletMultiplier, MagicWalletMaxDeposit, MagicWalletMaxCredit,
   CommissionMaxWeeklyBalancesPerLeg, CommissionMaxNetworkLevel)

Build: 0 errors, 610 warnings (40 new = expected Obsolete CS0618)
2026-02-25 22:32:42 +03:30
masoodafar-web fe3edd178d fix(B6): EXIT Magic Mode — reset PackagePurchaseMethod + close current cycle
B6: PackagePurchaseMethod was NOT reset when exiting Magic Mode.
This caused Guards G1-G3 to permanently block re-purchase.

Changes:
- user.PackagePurchaseMethod = None (allows re-purchase via G1-G3)
- currentCycle.IsCurrentCycle = false (ready for new cycle)

Note: membership.IsActive is intentionally preserved (Q19 — contract is one-time).
Features are preserved — DIFF will be applied on next purchase (Q20).
2026-02-25 21:59:30 +03:30
masoodafar-web 13dd0f552f refactor: archive 12 dead gRPC RPCs with [ARCHIVED] markers
Archived RPCs (code preserved, marked obsolete):
- fms.proto: excluded from csproj (CreateNewFileInfo, DeleteFileInfo)
- UserOrderService: GetOrdersByDateRange, CreateNewOrderForCustomer, SubmitOrderForCustomer
- ConfigurationService: DeactivateConfiguration, GetConfigurationHistory
- CityService: UpdateCity, DeleteCity
- HealthService: GetServiceHealth
- CategoryService: GetCategoryByIdForCustomer
- inventory.proto: BulkAdjustStock (no implementation existed)

All archived RPCs wrapped in #region [ARCHIVED] with [Obsolete] attributes.
No code deleted — archive only. Build passes with 0 errors.
2026-02-24 22:53:36 +03:30
masoodafar-web 8b9c317de6 fix: Phase 0 — critical bugs in package purchase handlers
B1: VerifyGoldenPackagePurchase — DiscountBalance was NOT charged (Amount × 2)
    Now charges DiscountBalance same as all other handlers.

B2: VerifyGoldenPackagePurchase — UserPackagePurchase record was NOT created
    Now creates UPP with OrderId and TransactionId.

B3: VerifyPackagePurchase — UserPackagePurchase record was NOT created
    Now creates UPP after wallet charge.

B4: VerifyBasePackagePayment — UserPackagePurchase record was NOT created
    Now creates UPP after wallet change log.

Also: VerifyGolden WalletChangeLog split into 2 logs (Balance + Discount)
      to match the pattern used by VerifyPackagePurchase.
      Transaction description changed from 'پکیج طلایی' to 'پکیج' (generic).

Impact: Only Daya Loan handler was creating UserPackagePurchase before.
        Now all 4 payment paths create it consistently.
2026-02-24 21:52:36 +03:30
96 changed files with 7260 additions and 1679 deletions
@@ -135,13 +135,25 @@ public class AcceptClubMembershipContractCommandHandler
};
await _context.UserContracts.AddAsync(userContract, cancellationToken);
// 6. دریافت مقادیر از SystemConstants (استاتیک)
long giftValue = SystemConstants.ClubMembershipGiftValue;
long activationFeeValue = SystemConstants.ClubActivationFee;
// 6. بارگذاری پکیج از آخرین خرید کاربر یا پکیج پایه
var latestPurchase = await _context.UserPackagePurchases
.Where(p => p.UserId == userId)
.OrderByDescending(p => p.PurchasedAt)
.FirstOrDefaultAsync(cancellationToken);
var package = latestPurchase != null
? await _context.Packages.FirstOrDefaultAsync(p => p.Id == latestPurchase.PackageId && !p.IsDeleted, cancellationToken)
: await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken);
if (package == null)
throw new NotFoundException("پکیج یافت نشد");
long giftValue = package.ActivationFee;
long activationFeeValue = package.ActivationFee;
_logger.LogInformation(
"Using Club.MembershipGiftValue: {GiftValue}, Club.ActivationFee: {ActivationFee}",
giftValue, activationFeeValue
"Using Package {PackageId} ({PackageName}): GiftValue={GiftValue}, ActivationFee={ActivationFee}",
package.Id, package.Title, giftValue, activationFeeValue
);
// 7. فعال‌سازی باشگاه مشتریان
@@ -156,6 +168,10 @@ public class AcceptClubMembershipContractCommandHandler
UserId = user.Id,
IsActive = true,
ActivatedAt = activationDate,
FirstActivationDate = activationDate,
FirstPackageId = package.Id,
LastActivationDate = activationDate,
LastPackageId = package.Id,
InitialContribution = activationFeeValue,
GiftValue = giftValue,
TotalEarned = 0,
@@ -175,6 +191,8 @@ public class AcceptClubMembershipContractCommandHandler
clubMembership = user.ClubMembership!;
clubMembership.IsActive = true;
clubMembership.ActivatedAt = activationDate;
clubMembership.LastActivationDate = activationDate;
clubMembership.LastPackageId = package.Id;
clubMembership.PurchaseMethod = user.PackagePurchaseMethod;
_context.ClubMemberships.Update(clubMembership);
@@ -206,13 +224,14 @@ public class AcceptClubMembershipContractCommandHandler
// 9. اضافه کردن مبلغ به Pool هفته جاری
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
var weeklyPool = await _context.WeeklyCommissionPools
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId, cancellationToken);
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
if (weeklyPool == null)
{
weeklyPool = new WeeklyCommissionPool
{
WeekDefinitionId = currentWeekDefinitionId,
PackageId = package.Id,
TotalPoolAmount = activationFeeValue,
TotalBalances = 0,
ValuePerBalance = 0,
@@ -54,6 +54,9 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
throw new NotFoundException(nameof(User), request.UserId);
}
// متغیر پکیج — در هر دو مسیر (عادی و Force) مقداردهی می‌شود
Package package;
// 2-5: بررسی‌های مالی — در حالت ForceActivation (ادمین) رد می‌شود
if (!request.ForceActivation)
{
@@ -61,11 +64,11 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
{
_logger.LogWarning(
"User {UserId} has not purchased golden package yet",
"User {UserId} has not purchased any package yet",
request.UserId
);
throw new BadRequestException(
"برای فعالسازی باشگاه مشتریان ابتدا باید پکیج طلایی خریداری کنید"
"برای فعالسازی باشگاه مشتریان ابتدا باید یک پکیج خریداری کنید"
);
}
@@ -79,17 +82,8 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
throw new NotFoundException("کیف پول کاربر یافت نشد");
}
if (wallet.Balance < SystemConstants.BasePackageAmount)
{
_logger.LogWarning(
"User {UserId} has insufficient balance: {Balance}",
request.UserId,
wallet.Balance
);
throw new BadRequestException(
$"برای فعالسازی باشگاه مشتریان باید حداقل {SystemConstants.BasePackageAmount:N0} ریال موجودی اصلی داشته باشید"
);
}
// NOTE: balance check uses package.Price — loaded after finding order (step 4)
// Moved to after package loading
// 3.5. بررسی وضعیت کیف‌پول جادویی — اگر در حالت Magic است، فعالسازی مجاز نیست
if (wallet.WalletMode == WalletMode.Magic)
@@ -119,7 +113,7 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
"No successful package order found for UserId: {UserId}",
request.UserId
);
throw new NotFoundException("سفارش پکیج طلایی یافت نشد");
throw new NotFoundException("سفارش پکیج یافت نشد");
}
// 5. بررسی Transaction
@@ -146,6 +140,19 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
"تراکنش معتبر برای فعالسازی باشگاه یافت نشد"
);
}
// 5.5. بارگذاری پکیج از سفارش
package = await _context.Packages
.FirstOrDefaultAsync(p => p.Id == packageOrder.PackageId && !p.IsDeleted, cancellationToken)
?? throw new NotFoundException("پکیج یافت نشد");
// بررسی موجودی با مبلغ پکیج واقعی
if (wallet.Balance < package.Price)
{
throw new BadRequestException(
$"برای فعالسازی باشگاه مشتریان باید حداقل {package.Price:N0} ریال موجودی اصلی داشته باشید"
);
}
}
else
{
@@ -161,19 +168,24 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
_context.Users.Update(user);
await _context.SaveChangesAsync(cancellationToken);
}
// بارگذاری پکیج پایه برای فعال‌سازی اجباری
package = await _context.Packages
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
?? throw new NotFoundException("پکیج پایه یافت نشد");
}
// 6. بررسی عضویت فعلی
var existingMembership = await _context.ClubMemberships
.FirstOrDefaultAsync(c => c.UserId == user.Id, cancellationToken);
// 6.1. دریافت مبلغ هدیه و هزینه فعالسازی از SystemConstants
long giftValue = SystemConstants.ClubMembershipGiftValue;
long activationFeeValue = SystemConstants.ClubActivationFee;
// 6.1. دریافت مبلغ هدیه و هزینه فعالسازی از پکیج
long giftValue = package.ActivationFee;
long activationFeeValue = package.ActivationFee;
_logger.LogInformation(
"Using Club.MembershipGiftValue: {GiftValue}, Club.ActivationFee: {ActivationFee}",
giftValue, activationFeeValue
"Using Package {PackageId} ({PackageName}): GiftValue={GiftValue}, ActivationFee={ActivationFee}, Price={Price}",
package.Id, package.Title, giftValue, activationFeeValue, package.Price
);
ClubMembership entity;
@@ -188,8 +200,12 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
UserId = user.Id,
IsActive = true,
ActivatedAt = activationDate,
InitialContribution =activationFeeValue,
GiftValue = giftValue, // مقدار از تنظیمات
FirstActivationDate = activationDate,
FirstPackageId = package.Id,
LastActivationDate = activationDate,
LastPackageId = package.Id,
InitialContribution = activationFeeValue,
GiftValue = giftValue,
TotalEarned = 0,
PurchaseMethod = user.PackagePurchaseMethod
};
@@ -214,9 +230,11 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
return true;
}
// فعال‌سازی مجدد — ActivatedAt حفظ میشه (overwrite نمیشه)
// فعال‌سازی مجدد — FirstActivation حفظ میشه (Q21), Last بروزرسانی میشه
entity = existingMembership;
entity.IsActive = true;
entity.LastActivationDate = activationDate;
entity.LastPackageId = package.Id;
entity.PurchaseMethod = user.PackagePurchaseMethod;
_context.ClubMemberships.Update(entity);
@@ -250,7 +268,8 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
CycleNumber = maxCycleNumber + 1,
PackagePurchasedAt = activationDate,
PurchaseMethod = user.PackagePurchaseMethod,
PackageAmount = SystemConstants.BasePackageAmount,
PackageAmount = package.Price,
PackageId = package.Id,
IsCurrentCycle = true
};
@@ -284,15 +303,16 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
// ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
var weeklyPool = await _context.WeeklyCommissionPools
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId, cancellationToken);
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
if (weeklyPool == null)
{
// ایجاد Pool جدید برای این هفته
// ایجاد Pool جدید برای این هفته × این پکیج
weeklyPool = new WeeklyCommissionPool
{
WeekDefinitionId = currentWeekDefinitionId,
TotalPoolAmount = activationFeeValue, // مبلغ هدیه به Pool اضافه میشه
PackageId = package.Id,
TotalPoolAmount = activationFeeValue,
TotalBalances = 0, // در CalculateWeeklyBalances محاسبه میشه
ValuePerBalance = 0, // در CalculateWeeklyCommissionPool محاسبه میشه
IsCalculated = false,
@@ -82,11 +82,14 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
var balancesList = new List<NetworkWeeklyBalance>();
var calculatedAt = DateTime.Now;
// استفاده از SystemConstants (استاتیک - بدون کوئری به دیتابیس)
// سقف تعادل هفتگی برای هر دست (نه کل) - 300 برای چپ + 300 برای راست = حداکثر 600 تعادل
var maxBalancesPerLeg = SystemConstants.CommissionMaxWeeklyBalancesPerLeg;
// حداکثر عمق شبکه برای شمارش اعضا (15 لول)
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
// بارگذاری پکیج پایه برای خواندن سقف‌ها
var package = await _context.Packages
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
?? throw new InvalidOperationException("پکیج پایه یافت نشد");
// خواندن سقف‌ها از پکیج (قبلاً از SystemConstants بود)
var maxBalancesPerLeg = package.MaxBalancesPerLeg;
var maxNetworkLevel = package.MaxNetworkLevel;
foreach (var user in usersInNetwork.OrderBy(o=>o.Id))
{
@@ -140,6 +143,7 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
{
UserId = user.Id,
WeekDefinitionId = request.WeekDefinitionId,
PackageId = package.Id,
// اطلاعات جدید
LeftLegNewMembers = leftNewMembers,
@@ -49,8 +49,11 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
await _context.SaveChangesAsync(cancellationToken);
}
// ⭐ خواندن MaxNetworkLevel از SystemConstants (استاتیک)
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
// بارگذاری پکیج پایه برای خواندن سقف‌ها
var package = await _context.Packages
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
?? throw new InvalidOperationException("پکیج پایه یافت نشد");
var maxNetworkLevel = package.MaxNetworkLevel;
// دریافت همه تعادل‌های هفتگی (شامل صفرها هم برای محاسبه زیرمجموعه)
var allWeeklyBalances = await _context.NetworkWeeklyBalances
@@ -113,6 +116,7 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
UserId = userId,
WeekDefinitionId = request.WeekDefinitionId,
WeeklyPoolId = pool.Id,
PackageId = package.Id,
BalancesEarned = totalBalancesWithSubordinates, // ⭐ شامل زیرمجموعه
ValuePerBalance = pool.ValuePerBalance,
TotalAmount = totalAmount,
@@ -18,6 +18,11 @@ public record GetMyCommissionPayoutsQuery : IRequest<GetMyCommissionPayoutsRespo
/// </summary>
public long? WeekDefinitionId { get; init; }
/// <summary>
/// فیلتر بر اساس پکیج (اختیاری)
/// </summary>
public long? PackageId { get; init; }
/// <summary>
/// Pagination
/// </summary>
@@ -28,6 +28,7 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissio
var query = _context.UserCommissionPayouts
.Include(x => x.WeekDefinition)
.Include(x => x.Package)
.Where(x => x.UserId == userId)
.AsNoTracking()
.AsQueryable();
@@ -43,6 +44,12 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissio
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId.Value);
}
// فیلتر بر اساس پکیج
if (request.PackageId.HasValue && request.PackageId.Value > 0)
{
query = query.Where(x => x.PackageId == request.PackageId.Value);
}
// مرتب‌سازی: جدیدترین اول
query = query.OrderByDescending(x => x.Created);
@@ -60,7 +67,9 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissio
AmountFormatted = x.TotalAmount.ToString("N0") + " تومان",
Status = x.Status,
CalculatedDate = x.PaidAt ?? (DateTime?)x.Created,
DatePersian = ""
DatePersian = "",
PackageId = x.PackageId,
PackageTitle = x.Package != null ? x.Package.Title : ""
})
.ToListAsync(cancellationToken);
@@ -20,4 +20,8 @@ public class GetMyCommissionPayoutsResponseModel
public CommissionPayoutStatus Status { get; set; }
public DateTime? CalculatedDate { get; set; }
public string DatePersian { get; set; } = string.Empty;
// Package info
public long PackageId { get; set; }
public string PackageTitle { get; set; } = string.Empty;
}
@@ -17,6 +17,11 @@ public record GetMyWeeklyBalancesQuery : IRequest<GetUserWeeklyBalancesResponseD
/// </summary>
public bool OnlyActive { get; init; }
/// <summary>
/// فیلتر بر اساس پکیج (اختیاری)
/// </summary>
public long? PackageId { get; init; }
/// <summary>
/// Pagination
/// </summary>
@@ -38,6 +38,7 @@ public class GetMyWeeklyBalancesQueryHandler : IRequestHandler<GetMyWeeklyBalanc
{
UserId = userId,
WeekDefinitionId = request.WeekDefinitionId,
PackageId = request.PackageId,
OnlyActive = request.OnlyActive,
PaginationState = request.PaginationState
};
@@ -20,6 +20,11 @@ public record GetUserCommissionPayoutsQuery : IRequest<GetUserCommissionPayoutsR
/// </summary>
public long? WeekDefinitionId { get; init; }
/// <summary>
/// فیلتر بر اساس پکیج (اختیاری)
/// </summary>
public long? PackageId { get; init; }
/// <summary>
/// مرتب‌سازی
/// </summary>
@@ -18,6 +18,7 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
var query = _context.UserCommissionPayouts
.Include(x => x.WeekDefinition)
.Include(x => x.User)
.Include(x => x.Package)
.AsNoTracking()
.AsQueryable();
@@ -41,6 +42,12 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId);
}
// فیلتر بر اساس پکیج
if (request.PackageId.HasValue && request.PackageId.Value > 0)
{
query = query.Where(x => x.PackageId == request.PackageId.Value);
}
query = query.ApplyOrder(sortBy: request.SortBy ?? "Created");
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
@@ -64,7 +71,9 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
WithdrawalMethod = x.WithdrawalMethod,
IbanNumber = x.IbanNumber,
WithdrawnAt = x.WithdrawnAt,
Created = x.Created
Created = x.Created,
PackageId = x.PackageId,
PackageTitle = x.Package != null ? x.Package.Title : ""
})
.ToListAsync(cancellationToken);
@@ -24,4 +24,8 @@ public class GetUserCommissionPayoutsResponseModel
public string? IbanNumber { get; set; }
public DateTime? WithdrawnAt { get; set; }
public DateTimeOffset Created { get; set; }
// Package info
public long PackageId { get; set; }
public string PackageTitle { get; set; } = string.Empty;
}
@@ -15,6 +15,10 @@ public record GetUserWeeklyBalancesQuery : IRequest<GetUserWeeklyBalancesRespons
/// </summary>
public long? WeekDefinitionId { get; init; }
/// <summary>
/// فیلتر بر اساس پکیج (اختیاری)
/// </summary>
public long? PackageId { get; init; }
/// <summary>
/// فقط موارد Expired نشده؟
@@ -18,6 +18,7 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
var query = _context.NetworkWeeklyBalances
.Include(x => x.WeekDefinition)
.Include(x => x.User)
.Include(x => x.Package)
.AsNoTracking()
.AsQueryable();
@@ -43,6 +44,12 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
query = query.Where(x => !x.IsExpired);
}
// فیلتر بر اساس پکیج
if (request.PackageId.HasValue && request.PackageId.Value > 0)
{
query = query.Where(x => x.PackageId == request.PackageId.Value);
}
// مرتب‌سازی بر اساس WeekDefinitionId (نزولی = جدیدترین اول)
query = query.ApplyOrder(sortBy: request.SortBy ?? "-WeekDefinitionId");
@@ -67,7 +74,9 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
WeeklyPoolContribution = x.WeeklyPoolContribution,
CalculatedAt = x.CalculatedAt,
IsExpired = x.IsExpired,
Created = x.Created
Created = x.Created,
PackageId = x.PackageId,
PackageTitle = x.Package != null ? x.Package.Title : ""
})
.ToListAsync(cancellationToken);
@@ -29,4 +29,8 @@ public class GetUserWeeklyBalancesResponseModel
public DateTime? CalculatedAt { get; set; }
public bool IsExpired { get; set; }
public DateTimeOffset Created { get; set; }
// Package info
public long PackageId { get; set; }
public string PackageTitle { get; set; } = string.Empty;
}
@@ -39,6 +39,7 @@ public interface IApplicationDbContext
DbSet<ClubMembership> ClubMemberships { get; }
DbSet<ClubMembershipHistory> ClubMembershipHistories { get; }
DbSet<ClubMembershipCycle> ClubMembershipCycles { get; }
DbSet<PackageFeature> PackageFeatures { get; }
DbSet<ClubFeature> ClubFeatures { get; }
DbSet<UserClubFeature> UserClubFeatures { get; }
DbSet<NetworkWeeklyBalance> NetworkWeeklyBalances { get; }
@@ -12,7 +12,7 @@ namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckAndProcessDayaLoa
/// 1. استعلام از API دایا
/// 2. ذخیره/به‌روزرسانی DayaLoanContract
/// 3. شارژ کیف پول برای وام‌های تأیید شده
/// 4. ثبت Order پکیج طلایی
/// 4. ثبت Order پکیج
/// 5. ارسال SMS
/// </summary>
public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndProcessDayaLoansCommand, CheckAndProcessDayaLoansResponseDto>
@@ -186,10 +186,15 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
string contractNumber,
CancellationToken cancellationToken)
{
// بارگذاری پکیج پایه
var package = await _context.Packages
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
?? throw new InvalidOperationException("پکیج پایه یافت نشد");
// 1. ایجاد تراکنش
var transaction = new Transaction
{
Amount = SystemConstants.DayaLoanAmount,
Amount = package.Price,
Description = $"دریافت اعتبار دایا - قرارداد {contractNumber}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.Now,
@@ -216,13 +221,13 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
}
// 3. شارژ کیف پول عادی
wallet.Balance += SystemConstants.DayaLoanAmount;
wallet.Balance += package.Price;
var mainLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = 0,
ChangeValue = SystemConstants.DayaLoanAmount,
ChangeValue = package.Price,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
IsIncrease = true,
@@ -230,8 +235,8 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
};
await _context.UserWalletChangeLogs.AddAsync(mainLog, cancellationToken);
// 4. شارژ کیف پول تخفیف (دو برابر)
var discountAmount = SystemConstants.DayaLoanAmount * 2;
// 4. شارژ کیف پول تخفیف
var discountAmount = (long)(package.Price * package.DiscountMultiplier);
wallet.DiscountBalance += discountAmount;
var discountLog = new UserWalletChangeLog
@@ -253,27 +258,18 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
user.DayaCreditReceivedAt = DateTime.Now;
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan;
// 6. ثبت Order پکیج پایه
var goldenPackage = await _context.Packages
.FirstOrDefaultAsync(p => p.Id == 4, cancellationToken);
if (goldenPackage != null)
// 6. ثبت UserPackagePurchase برای پکیج پایه
var packagePurchase = new UserPackagePurchase
{
// 6. ثبت UserPackagePurchase برای پکیج پایه
var goldenPackageId =goldenPackage.Id;
var packagePurchase = new UserPackagePurchase
{
UserId = user.Id,
PackageId = goldenPackageId,
PurchaseMethod = PackagePurchaseMethod.DayaLoan,
PurchasedAt = DateTime.Now,
Amount = SystemConstants.DayaLoanAmount,
TransactionId = transaction.Id
};
UserId = user.Id,
PackageId = package.Id,
PurchaseMethod = PackagePurchaseMethod.DayaLoan,
PurchasedAt = DateTime.Now,
Amount = package.Price,
TransactionId = transaction.Id
};
await _context.UserPackagePurchases.AddAsync(packagePurchase, cancellationToken);
}
await _context.UserPackagePurchases.AddAsync(packagePurchase, cancellationToken);
// 7. Domain Event
user.AddDomainEvent(new DayaLoanApprovedEvent(user, transaction, contractNumber));
@@ -70,9 +70,13 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
throw new NotFoundException($"کیف پول کاربر {request.UserId} یافت نشد");
}
// 4. محاسبه مبالغ
var balanceAmount = SystemConstants.BasePackageAmount; // 56M
var discountBalanceAmount = SystemConstants.BasePackageAmount * 2; // 112M
// 4. بارگذاری پکیج پایه و محاسبه مبالغ
var package = await _context.Packages
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
?? throw new NotFoundException("پکیج پایه یافت نشد");
var balanceAmount = package.Price;
var discountBalanceAmount = (long)(package.Price * package.DiscountMultiplier);
// 5. ثبت تراکنش
@@ -112,8 +116,8 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
var oldBalance = wallet.Balance;
var oldDiscountBalance = wallet.DiscountBalance;
wallet.Balance += balanceAmount; // +56M
wallet.DiscountBalance += discountBalanceAmount; // +112M
wallet.Balance += balanceAmount;
wallet.DiscountBalance += discountBalanceAmount;
// 8. ثبت لاگ کیف پول
var walletLog = new UserWalletChangeLog
@@ -143,7 +143,7 @@ public class GetUserNetworkPositionQueryHandler : IRequestHandler<GetUserNetwork
HasReceivedDayaCredit = user.HasReceivedDayaCredit,
DayaCreditReceivedAt = user.DayaCreditReceivedAt,
PackagePurchaseMethod = user.PackagePurchaseMethod,
HasPurchasedGoldenPackage = user.PackagePurchaseMethod != PackagePurchaseMethod.None,
HasPurchasedPackage = user.PackagePurchaseMethod != PackagePurchaseMethod.None,
// آمار مالی
TotalEarnedCommission = commissionStats?.TotalAmount ?? 0,
@@ -49,7 +49,7 @@ public class UserNetworkPositionDto
public bool HasReceivedDayaCredit { get; set; }
public DateTime? DayaCreditReceivedAt { get; set; }
public PackagePurchaseMethod PackagePurchaseMethod { get; set; }
public bool HasPurchasedGoldenPackage { get; set; }
public bool HasPurchasedPackage { get; set; }
// آمار مالی
public decimal TotalEarnedCommission { get; set; }
@@ -9,5 +9,18 @@ public record CreateNewPackageCommand : IRequest<CreateNewPackageResponseDto>
public string ImagePath { get; init; }
//قیمت
public long Price { get; init; }
// فیلدهای جدید پکیج
public int SortOrder { get; init; }
public bool IsActive { get; init; } = true;
public bool IsBasePackage { get; init; }
public bool SupportsDayaPurchase { get; init; } = true;
public bool SupportsDirectPurchase { get; init; } = true;
public long ActivationFee { get; init; }
public double DiscountMultiplier { get; init; } = 2.0;
public double MagicWalletMultiplier { get; init; } = 2.5;
public int MaxBalancesPerLeg { get; init; } = 300;
public int MaxNetworkLevel { get; init; } = 15;
public long MagicWalletMaxDeposit { get; init; } = 1_000_000_000;
public long MagicWalletMaxCredit { get; init; } = 2_500_000_000;
public List<long> FeatureIds { get; init; } = new();
}
@@ -16,6 +16,22 @@ public class CreateNewPackageCommandHandler : IRequestHandler<CreateNewPackageCo
await _context.Packages.AddAsync(entity, cancellationToken);
entity.AddDomainEvent(new CreateNewPackageEvent(entity));
await _context.SaveChangesAsync(cancellationToken);
// Sync PackageFeatures
if (request.FeatureIds.Any())
{
foreach (var featureId in request.FeatureIds)
{
_context.PackageFeatures.Add(new PackageFeature
{
PackageId = entity.Id,
ClubFeatureId = featureId,
IsIncluded = true
});
}
await _context.SaveChangesAsync(cancellationToken);
}
return entity.Adapt<CreateNewPackageResponseDto>();
}
}
@@ -1,43 +0,0 @@
using MediatR;
namespace CMSMicroservice.Application.PackageCQ.Commands.InitiateBasePackagePayment;
/// <summary>
/// ثبت اولیه تراکنش و سفارش پکیج پایه (56 میلیون تومان)
/// این Command توسط BFF فراخوانی می‌شود قبل از ارسال کاربر به درگاه
/// </summary>
public record InitiateBasePackagePaymentCommand : IRequest<InitiateBasePackagePaymentResponseDto>
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; init; }
}
public class InitiateBasePackagePaymentResponseDto
{
/// <summary>
/// موفقیت عملیات
/// </summary>
public bool Success { get; set; }
/// <summary>
/// پیام
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// شناسه سفارش
/// </summary>
public long OrderId { get; set; }
/// <summary>
/// شناسه تراکنش CMS
/// </summary>
public long TransactionId { get; set; }
/// <summary>
/// مبلغ پکیج
/// </summary>
public long Amount { get; set; }
}
@@ -1,169 +0,0 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Common;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using ValidationException = FluentValidation.ValidationException;
namespace CMSMicroservice.Application.PackageCQ.Commands.InitiateBasePackagePayment;
/// <summary>
/// Handler برای ثبت اولیه تراکنش و سفارش پکیج پایه
/// این Handler فقط Transaction و Order را با وضعیت Pending ایجاد می‌کند
/// BFF سپس کاربر را به درگاه PYMS هدایت می‌کند
/// </summary>
public class InitiateBasePackagePaymentCommandHandler : IRequestHandler<InitiateBasePackagePaymentCommand, InitiateBasePackagePaymentResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly ILogger<InitiateBasePackagePaymentCommandHandler> _logger;
/// <summary>
/// شناسه پکیج پایه در دیتابیس
/// </summary>
private const long BasePackageId = 4;
public InitiateBasePackagePaymentCommandHandler(
IApplicationDbContext context,
ILogger<InitiateBasePackagePaymentCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<InitiateBasePackagePaymentResponseDto> Handle(
InitiateBasePackagePaymentCommand request,
CancellationToken cancellationToken)
{
try
{
_logger.LogInformation(
"Initiating base package payment for UserId: {UserId}",
request.UserId);
// 1. پیدا کردن کاربر
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
if (user == null)
{
_logger.LogWarning("User not found. UserId: {UserId}", request.UserId);
throw new NotFoundException(nameof(User), request.UserId);
}
// 2. بررسی عدم خرید قبلی پکیج پایه
if (user.PackagePurchaseMethod != PackagePurchaseMethod.None)
{
_logger.LogWarning(
"User {UserId} has already purchased base package via {Method}",
request.UserId,
user.PackagePurchaseMethod);
return new InitiateBasePackagePaymentResponseDto
{
Success = false,
Message = "شما قبلاً پکیج پایه را خریداری کرده‌اید."
};
}
// 3. بررسی عدم وجود سفارش Pending
var pendingOrder = await _context.UserOrders
.FirstOrDefaultAsync(o =>
o.UserId == request.UserId &&
o.PaymentStatus == PaymentStatus.Pending &&
o.PackageId == BasePackageId,
cancellationToken);
if (pendingOrder != null)
{
_logger.LogWarning(
"User {UserId} has pending order {OrderId}",
request.UserId,
pendingOrder.Id);
// برگرداندن سفارش موجود برای ادامه پرداخت
var existingTransaction = await _context.Transactions
.FirstOrDefaultAsync(t => t.Id == pendingOrder.TransactionId, cancellationToken);
return new InitiateBasePackagePaymentResponseDto
{
Success = true,
Message = "سفارش قبلی در انتظار پرداخت یافت شد.",
OrderId = pendingOrder.Id,
TransactionId = existingTransaction?.Id ?? 0,
Amount = SystemConstants.BasePackageAmount
};
}
// 4. پیدا کردن آدرس پیش‌فرض کاربر
var defaultAddress = await _context.UserAddresses
.Where(a => a.UserId == request.UserId)
.OrderByDescending(a => a.Created)
.FirstOrDefaultAsync(cancellationToken);
if (defaultAddress == null)
{
_logger.LogWarning("No address found for user {UserId}", request.UserId);
throw new ValidationException("لطفاً ابتدا یک آدرس برای خود ثبت کنید.");
}
// 5. ایجاد Transaction با وضعیت Pending
var transaction = new Transaction
{
Amount = SystemConstants.BasePackageAmount,
Description = $"خرید پکیج پایه ۵۶ میلیونی - کاربر #{user.Id}",
PaymentStatus = PaymentStatus.Pending,
Type = TransactionType.DepositIpg
};
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Created Transaction {TransactionId} with Pending status for UserId {UserId}",
transaction.Id,
request.UserId);
// 6. ایجاد UserOrder با وضعیت Pending
var order = new UserOrder
{
UserId = user.Id,
PackageId = BasePackageId,
Amount = SystemConstants.BasePackageAmount,
PaymentStatus = PaymentStatus.Pending,
DeliveryStatus = DeliveryStatus.None,
UserAddressId = defaultAddress.Id,
PaymentMethod = PaymentMethod.IPG,
TransactionId = transaction.Id
};
_context.UserOrders.Add(order);
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Created UserOrder {OrderId} for UserId {UserId}, TransactionId: {TransactionId}",
order.Id,
request.UserId,
transaction.Id);
return new InitiateBasePackagePaymentResponseDto
{
Success = true,
Message = "تراکنش و سفارش با موفقیت ثبت شد.",
OrderId = order.Id,
TransactionId = transaction.Id,
Amount = SystemConstants.BasePackageAmount
};
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in InitiateBasePackagePaymentCommand for UserId: {UserId}",
request.UserId);
throw;
}
}
}
@@ -1,13 +0,0 @@
using FluentValidation;
namespace CMSMicroservice.Application.PackageCQ.Commands.InitiateBasePackagePayment;
public class InitiateBasePackagePaymentCommandValidator : AbstractValidator<InitiateBasePackagePaymentCommand>
{
public InitiateBasePackagePaymentCommandValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر معتبر نیست.");
}
}
@@ -1,22 +0,0 @@
using MediatR;
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
/// <summary>
/// خرید پکیج طلایی (شروع فرآیند پرداخت)
/// </summary>
public record PurchaseGoldenPackageCommand : IRequest<PurchaseGoldenPackageResponseDto>
{
public long UserId { get; init; }
public long PackageId { get; init; }
public string ReturnUrl { get; init; } = string.Empty;
}
public class PurchaseGoldenPackageResponseDto
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public long OrderId { get; set; }
public string PaymentGatewayUrl { get; set; } = string.Empty;
public string TrackingCode { get; set; } = string.Empty;
}
@@ -1,161 +0,0 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using ValidationException = FluentValidation.ValidationException;
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
public class PurchaseGoldenPackageCommandHandler : IRequestHandler<PurchaseGoldenPackageCommand, PurchaseGoldenPackageResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ILogger<PurchaseGoldenPackageCommandHandler> _logger;
public PurchaseGoldenPackageCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
ILogger<PurchaseGoldenPackageCommandHandler> logger)
{
_context = context;
_paymentGateway = paymentGateway;
_logger = logger;
}
public async Task<PurchaseGoldenPackageResponseDto> Handle(PurchaseGoldenPackageCommand request, CancellationToken cancellationToken)
{
try
{
_logger.LogInformation(
"Starting golden package purchase for UserId: {UserId}, PackageId: {PackageId}",
request.UserId,
request.PackageId);
// 1. پیدا کردن کاربر
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
if (user == null)
{
_logger.LogWarning("User not found for golden package purchase. UserId: {UserId}", request.UserId);
throw new NotFoundException(nameof(User), request.UserId);
}
// 2. جلوگیری از خرید مجدد پکیج طلایی
if (user.PackagePurchaseMethod != PackagePurchaseMethod.None)
{
_logger.LogWarning(
"User {UserId} has already purchased golden package via {Method}",
request.UserId,
user.PackagePurchaseMethod);
throw new ValidationException("شما قبلاً پکیج طلایی را خریداری کرده‌اید.");
}
// 3. پیدا کردن پکیج
var package = await _context.Packages
.FirstOrDefaultAsync(p => p.Id == request.PackageId, cancellationToken);
if (package == null)
{
_logger.LogWarning("Golden package not found. PackageId: {PackageId}", request.PackageId);
throw new NotFoundException(nameof(Package), request.PackageId);
}
// اطمینان از اینکه این همان پکیج طلایی است
if (!package.Title.Contains("طلایی", StringComparison.OrdinalIgnoreCase) &&
!package.Title.Contains("golden", StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning(
"PackageId {PackageId} is not a golden package. Title: {Title}",
request.PackageId,
package.Title);
throw new ValidationException("فقط پکیج طلایی قابل خرید است.");
}
// 4. پیدا کردن آدرس پیش‌فرض کاربر (الزامی برای UserOrder)
var defaultAddress = await _context.UserAddresses
.Where(a => a.UserId == request.UserId)
.OrderByDescending(a => a.Created)
.FirstOrDefaultAsync(cancellationToken);
if (defaultAddress == null)
{
_logger.LogWarning("No address found for user {UserId} in golden package purchase", request.UserId);
throw new ValidationException("لطفاً ابتدا یک آدرس برای خود ثبت کنید.");
}
// 5. ایجاد سفارش
var order = new UserOrder
{
UserId = user.Id,
PackageId = package.Id,
Amount = package.Price,
PaymentStatus = PaymentStatus.Pending,
DeliveryStatus = DeliveryStatus.None,
UserAddressId = defaultAddress.Id,
PaymentMethod = PaymentMethod.IPG
};
_context.UserOrders.Add(order);
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Created golden package UserOrder {OrderId} for UserId {UserId}, Amount: {Amount}",
order.Id,
request.UserId,
order.Amount);
// 6. شروع پرداخت با درگاه
var paymentRequest = new PaymentRequest
{
Amount = order.Amount,
UserId = user.Id,
Mobile = user.Mobile ?? string.Empty,
CallbackUrl = request.ReturnUrl,
Description = $"خرید پکیج طلایی - سفارش #{order.Id}"
};
var paymentResult = await _paymentGateway.InitiatePaymentAsync(paymentRequest, cancellationToken);
if (!paymentResult.IsSuccess)
{
_logger.LogError(
"Payment gateway initiation failed for golden package. OrderId {OrderId}: {ErrorMessage}",
order.Id,
paymentResult.ErrorMessage);
order.PaymentStatus = PaymentStatus.Reject;
await _context.SaveChangesAsync(cancellationToken);
throw new Exception($"خطا در ارتباط با درگاه پرداخت: {paymentResult.ErrorMessage}");
}
_logger.LogInformation(
"Golden package payment initiated successfully. OrderId: {OrderId}, RefId: {RefId}",
order.Id,
paymentResult.RefId);
return new PurchaseGoldenPackageResponseDto
{
Success = true,
Message = "لطفاً به درگاه پرداخت منتقل شوید.",
OrderId = order.Id,
PaymentGatewayUrl = paymentResult.GatewayUrl ?? string.Empty,
TrackingCode = paymentResult.RefId ?? string.Empty
};
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in PurchaseGoldenPackageCommand for UserId: {UserId}",
request.UserId);
throw;
}
}
}
@@ -1,23 +0,0 @@
using FluentValidation;
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
public class PurchaseGoldenPackageCommandValidator : AbstractValidator<PurchaseGoldenPackageCommand>
{
public PurchaseGoldenPackageCommandValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از 0 باشد");
RuleFor(x => x.PackageId)
.GreaterThan(0)
.WithMessage("شناسه پکیج باید بزرگتر از 0 باشد");
RuleFor(x => x.ReturnUrl)
.NotEmpty()
.WithMessage("آدرس بازگشت الزامی است")
.Must(url => Uri.TryCreate(url, UriKind.Absolute, out _))
.WithMessage("آدرس بازگشت معتبر نیست");
}
}
@@ -1,18 +0,0 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchasePackage;
/// <summary>
/// دستور خرید پکیج از طریق درگاه بانکی
/// </summary>
public class PurchasePackageCommand : IRequest<PaymentInitiateResult>
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; set; }
}
@@ -1,164 +0,0 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using ValidationException = FluentValidation.ValidationException;
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchasePackage;
public class PurchasePackageCommandHandler
: IRequestHandler<PurchasePackageCommand, PaymentInitiateResult>
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly IConfiguration _configuration;
private readonly ILogger<PurchasePackageCommandHandler> _logger;
public PurchasePackageCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
IConfiguration configuration,
ILogger<PurchasePackageCommandHandler> logger)
{
_context = context;
_paymentGateway = paymentGateway;
_configuration = configuration;
_logger = logger;
}
public async Task<PaymentInitiateResult> Handle(
PurchasePackageCommand request,
CancellationToken cancellationToken)
{
try
{
_logger.LogInformation(
"Starting package purchase for UserId: {UserId}",
request.UserId
);
// 1. بررسی وجود کاربر
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
if (user == null)
{
_logger.LogWarning("User not found: {UserId}", request.UserId);
throw new NotFoundException(nameof(User), request.UserId);
}
// 2. بررسی اینکه قبلاً پکیج نخریده باشد
if (user.PackagePurchaseMethod != PackagePurchaseMethod.None)
{
_logger.LogWarning(
"User {UserId} has already purchased package via {Method}",
request.UserId,
user.PackagePurchaseMethod
);
throw new ValidationException(
"شما قبلاً پکیج را خریداری کرده‌اید"
);
}
// 3. پیدا کردن پکیج (فعلاً پکیج طلایی)
var goldenPackage = await _context.Packages
.FirstOrDefaultAsync(
p => p.Title.Contains("طلایی") || p.Title.Contains("Golden"),
cancellationToken
);
if (goldenPackage == null)
{
_logger.LogError("Package not found in database");
throw new NotFoundException("پکیج یافت نشد");
}
// 4. پیدا کردن آدرس پیش‌فرض کاربر (برای فیلد اجباری)
var defaultAddress = await _context.UserAddresses
.Where(a => a.UserId == request.UserId)
.OrderByDescending(a => a.Created)
.FirstOrDefaultAsync(cancellationToken);
if (defaultAddress == null)
{
_logger.LogWarning("No address found for user {UserId}", request.UserId);
throw new ValidationException(
"لطفاً ابتدا یک آدرس برای خود ثبت کنید"
);
}
// 5. ایجاد سفارش
var order = new UserOrder
{
UserId = user.Id,
PackageId = goldenPackage.Id,
Amount = goldenPackage.Price, // 56,000,000 تومان
PaymentStatus = PaymentStatus.Pending,
DeliveryStatus = DeliveryStatus.None,
UserAddressId = defaultAddress.Id,
PaymentMethod = PaymentMethod.IPG
};
_context.UserOrders.Add(order);
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Created UserOrder {OrderId} for UserId {UserId}, Amount: {Amount}",
order.Id,
request.UserId,
order.Amount
);
// 6. ایجاد درخواست پرداخت از درگاه
var paymentRequest = new PaymentRequest
{
Amount = order.Amount,
UserId = user.Id,
Mobile = user.Mobile ?? "",
CallbackUrl = $"{_configuration["CmsBaseUrl"] ?? "https://localhost:32846"}/api/package/verify-package",
Description = $"خرید پکیج - سفارش #{order.Id}"
};
var paymentResult = await _paymentGateway.InitiatePaymentAsync(paymentRequest);
if (!paymentResult.IsSuccess)
{
_logger.LogError(
"Payment gateway failed for OrderId {OrderId}: {ErrorMessage}",
order.Id,
paymentResult.ErrorMessage
);
// به‌روزرسانی وضعیت سفارش
order.PaymentStatus = PaymentStatus.Reject;
await _context.SaveChangesAsync(cancellationToken);
throw new Exception(
$"خطا در ارتباط با درگاه پرداخت: {paymentResult.ErrorMessage}"
);
}
_logger.LogInformation(
"Payment initiated successfully. OrderId: {OrderId}, RefId: {RefId}",
order.Id,
paymentResult.RefId
);
return paymentResult;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in PurchasePackageCommand for UserId: {UserId}",
request.UserId
);
throw;
}
}
}
@@ -1,13 +0,0 @@
using FluentValidation;
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchasePackage;
public class PurchasePackageCommandValidator : AbstractValidator<PurchasePackageCommand>
{
public PurchasePackageCommandValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
}
}
@@ -11,5 +11,18 @@ public record UpdatePackageCommand : IRequest<Unit>
public string ImagePath { get; init; }
//قیمت
public long Price { get; init; }
// فیلدهای جدید پکیج
public int SortOrder { get; init; }
public bool IsActive { get; init; }
public bool IsBasePackage { get; init; }
public bool SupportsDayaPurchase { get; init; }
public bool SupportsDirectPurchase { get; init; }
public long ActivationFee { get; init; }
public double DiscountMultiplier { get; init; }
public double MagicWalletMultiplier { get; init; }
public int MaxBalancesPerLeg { get; init; }
public int MaxNetworkLevel { get; init; }
public long MagicWalletMaxDeposit { get; init; }
public long MagicWalletMaxCredit { get; init; }
public List<long> FeatureIds { get; init; } = new();
}
@@ -16,6 +16,23 @@ public class UpdatePackageCommandHandler : IRequestHandler<UpdatePackageCommand,
request.Adapt(entity);
_context.Packages.Update(entity);
entity.AddDomainEvent(new UpdatePackageEvent(entity));
// Sync PackageFeatures — remove old, add new
var existingFeatures = await _context.PackageFeatures
.Where(pf => pf.PackageId == request.Id)
.ToListAsync(cancellationToken);
_context.PackageFeatures.RemoveRange(existingFeatures);
foreach (var featureId in request.FeatureIds)
{
_context.PackageFeatures.Add(new PackageFeature
{
PackageId = request.Id,
ClubFeatureId = featureId,
IsIncluded = true
});
}
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
@@ -1,72 +0,0 @@
using MediatR;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyBasePackagePayment;
/// <summary>
/// تکمیل یا رد پرداخت پکیج پایه (فراخوانی توسط BFF بعد از Callback)
/// </summary>
public record VerifyBasePackagePaymentCommand : IRequest<VerifyBasePackagePaymentResponseDto>
{
/// <summary>
/// شناسه سفارش CMS
/// </summary>
public long OrderId { get; init; }
/// <summary>
/// شناسه تراکنش CMS
/// </summary>
public long TransactionId { get; init; }
/// <summary>
/// آیا پرداخت موفق بوده؟ (نتیجه Verify از BFF)
/// </summary>
public bool PaymentSuccess { get; init; }
/// <summary>
/// کد پیگیری بانکی (در صورت موفقیت)
/// </summary>
public string? RefId { get; init; }
/// <summary>
/// پیام از درگاه
/// </summary>
public string? Message { get; init; }
}
public class VerifyBasePackagePaymentResponseDto
{
/// <summary>
/// موفقیت عملیات
/// </summary>
public bool Success { get; set; }
/// <summary>
/// پیام
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// شناسه سفارش
/// </summary>
public long OrderId { get; set; }
/// <summary>
/// شناسه تراکنش
/// </summary>
public long TransactionId { get; set; }
/// <summary>
/// کد پیگیری بانکی
/// </summary>
public string? ReferenceCode { get; set; }
/// <summary>
/// موجودی کیف پول بعد از شارژ
/// </summary>
public long WalletBalance { get; set; }
/// <summary>
/// موجودی تخفیف بعد از شارژ
/// </summary>
public long DiscountBalance { get; set; }
}
@@ -1,205 +0,0 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Common;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyBasePackagePayment;
/// <summary>
/// Handler برای تکمیل یا رد پرداخت پکیج پایه
/// فراخوانی توسط BFF بعد از اینکه نتیجه Verify از PYMS مشخص شد
///
/// اگر PaymentSuccess = true:
/// - شارژ کیف پول (Balance + DiscountBalance)
/// - ثبت لاگ تغییر کیف پول
/// - به‌روزرسانی سفارش و کاربر
///
/// اگر PaymentSuccess = false:
/// - فقط آپدیت وضعیت تراکنش و سفارش به Reject
/// </summary>
public class VerifyBasePackagePaymentCommandHandler : IRequestHandler<VerifyBasePackagePaymentCommand, VerifyBasePackagePaymentResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly ILogger<VerifyBasePackagePaymentCommandHandler> _logger;
public VerifyBasePackagePaymentCommandHandler(
IApplicationDbContext context,
ILogger<VerifyBasePackagePaymentCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<VerifyBasePackagePaymentResponseDto> Handle(
VerifyBasePackagePaymentCommand request,
CancellationToken cancellationToken)
{
try
{
_logger.LogInformation(
"Processing base package payment result. OrderId: {OrderId}, TransactionId: {TransactionId}, PaymentSuccess: {PaymentSuccess}",
request.OrderId,
request.TransactionId,
request.PaymentSuccess);
// 1. پیدا کردن سفارش با کاربر
var order = await _context.UserOrders
.Include(o => o.User)
.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
if (order == null)
{
_logger.LogWarning("Order not found. OrderId: {OrderId}", request.OrderId);
throw new NotFoundException(nameof(UserOrder), request.OrderId);
}
// 2. پیدا کردن تراکنش
var transaction = await _context.Transactions
.FirstOrDefaultAsync(t => t.Id == request.TransactionId, cancellationToken);
if (transaction == null)
{
_logger.LogWarning("Transaction not found. TransactionId: {TransactionId}", request.TransactionId);
throw new NotFoundException(nameof(Transaction), request.TransactionId);
}
// 3. بررسی Idempotency - اگر قبلاً تایید شده باشد
if (order.PaymentStatus == PaymentStatus.Success)
{
_logger.LogInformation(
"Order {OrderId} already verified successfully",
request.OrderId);
var existingWallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == order.UserId, cancellationToken);
return new VerifyBasePackagePaymentResponseDto
{
Success = true,
Message = "پرداخت قبلاً با موفقیت تایید شده است.",
OrderId = order.Id,
TransactionId = transaction.Id,
ReferenceCode = transaction.RefId,
WalletBalance = existingWallet?.Balance ?? 0,
DiscountBalance = existingWallet?.DiscountBalance ?? 0
};
}
// 4. اگر پرداخت ناموفق بود - فقط وضعیت را Reject می‌کنیم
if (!request.PaymentSuccess)
{
_logger.LogWarning(
"Payment failed. OrderId: {OrderId}, Message: {Message}",
request.OrderId,
request.Message);
transaction.PaymentStatus = PaymentStatus.Reject;
order.PaymentStatus = PaymentStatus.Reject;
await _context.SaveChangesAsync(cancellationToken);
return new VerifyBasePackagePaymentResponseDto
{
Success = false,
Message = request.Message ?? "پرداخت ناموفق بود.",
OrderId = order.Id,
TransactionId = transaction.Id
};
}
// 5. پرداخت موفق - پیدا کردن یا ایجاد کیف پول
var userWallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == order.UserId, cancellationToken);
if (userWallet == null)
{
_logger.LogInformation("Creating new wallet for UserId: {UserId}", order.UserId);
userWallet = new UserWallet
{
UserId = order.UserId,
Balance = 0,
DiscountBalance = 0,
NetworkBalance = 0
};
_context.UserWallets.Add(userWallet);
await _context.SaveChangesAsync(cancellationToken);
}
// 6. شارژ کیف پول (هم Balance و هم DiscountBalance)
var oldBalance = userWallet.Balance;
var oldDiscountBalance = userWallet.DiscountBalance;
userWallet.Balance += SystemConstants.BasePackageAmount;
userWallet.DiscountBalance += SystemConstants.BasePackageAmount * 2;
_logger.LogInformation(
"Charging wallet for user {UserId}. Balance: {OldBalance} -> {NewBalance}, DiscountBalance: {OldDiscount} -> {NewDiscount}",
order.UserId,
oldBalance,
userWallet.Balance,
oldDiscountBalance,
userWallet.DiscountBalance);
// 7. به‌روزرسانی Transaction
transaction.PaymentStatus = PaymentStatus.Success;
transaction.PaymentDate = DateTime.Now;
transaction.RefId = request.RefId;
// 8. ثبت لاگ تغییر کیف پول
var changeLog = new UserWalletChangeLog
{
WalletId = userWallet.Id,
CurrentBalance = userWallet.Balance,
ChangeValue = SystemConstants.BasePackageAmount,
CurrentNetworkBalance = userWallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = userWallet.DiscountBalance,
ChangeDiscountValue = SystemConstants.BasePackageAmount * 2,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(changeLog, cancellationToken);
// 9. به‌روزرسانی Order
order.TransactionId = transaction.Id;
order.PaymentStatus = PaymentStatus.Success;
order.PaymentDate = DateTime.Now;
order.PaymentMethod = PaymentMethod.IPG;
// 10. به‌روزرسانی User
order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Base package payment completed successfully. OrderId: {OrderId}, UserId: {UserId}, TransactionId: {TransactionId}, RefId: {RefId}",
order.Id,
order.UserId,
transaction.Id,
request.RefId);
return new VerifyBasePackagePaymentResponseDto
{
Success = true,
Message = "پرداخت با موفقیت تایید شد. کیف پول شما شارژ گردید.",
OrderId = order.Id,
TransactionId = transaction.Id,
ReferenceCode = request.RefId,
WalletBalance = userWallet.Balance,
DiscountBalance = userWallet.DiscountBalance
};
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in VerifyBasePackagePaymentCommand. OrderId: {OrderId}",
request.OrderId);
throw;
}
}
}
@@ -1,17 +0,0 @@
using FluentValidation;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyBasePackagePayment;
public class VerifyBasePackagePaymentCommandValidator : AbstractValidator<VerifyBasePackagePaymentCommand>
{
public VerifyBasePackagePaymentCommandValidator()
{
RuleFor(x => x.OrderId)
.GreaterThan(0)
.WithMessage("شناسه سفارش معتبر نیست.");
RuleFor(x => x.TransactionId)
.GreaterThan(0)
.WithMessage("شناسه تراکنش معتبر نیست.");
}
}
@@ -1,23 +0,0 @@
using MediatR;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
/// <summary>
/// تایید پرداخت پکیج طلایی (پس از بازگشت از درگاه)
/// </summary>
public record VerifyGoldenPackagePurchaseCommand : IRequest<VerifyGoldenPackagePurchaseResponseDto>
{
public long OrderId { get; init; }
public string Authority { get; init; } = string.Empty;
public string Status { get; init; } = string.Empty; // OK یا NOK
}
public class VerifyGoldenPackagePurchaseResponseDto
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public long OrderId { get; set; }
public long TransactionId { get; set; }
public string ReferenceCode { get; set; } = string.Empty;
public long WalletBalance { get; set; }
}
@@ -1,187 +0,0 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using ValidationException = FluentValidation.ValidationException;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
public class VerifyGoldenPackagePurchaseCommandHandler : IRequestHandler<VerifyGoldenPackagePurchaseCommand, VerifyGoldenPackagePurchaseResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ILogger<VerifyGoldenPackagePurchaseCommandHandler> _logger;
public VerifyGoldenPackagePurchaseCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
ILogger<VerifyGoldenPackagePurchaseCommandHandler> logger)
{
_context = context;
_paymentGateway = paymentGateway;
_logger = logger;
}
public async Task<VerifyGoldenPackagePurchaseResponseDto> Handle(VerifyGoldenPackagePurchaseCommand request, CancellationToken cancellationToken)
{
try
{
_logger.LogInformation(
"Verifying golden package purchase. OrderId: {OrderId}, Authority: {Authority}, Status: {Status}",
request.OrderId,
request.Authority,
request.Status);
// 1. اگر پرداخت از سمت درگاه موفق گزارش نشده باشد
if (!string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase))
{
var pendingOrder = await _context.UserOrders
.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
if (pendingOrder != null && pendingOrder.PaymentStatus == PaymentStatus.Pending)
{
pendingOrder.PaymentStatus = PaymentStatus.Reject;
await _context.SaveChangesAsync(cancellationToken);
}
throw new ValidationException("پرداخت توسط کاربر لغو شد.");
}
// 2. پیدا کردن سفارش به همراه کاربر
var order = await _context.UserOrders
.Include(o => o.User)
.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
if (order == null)
{
_logger.LogWarning("Golden package order not found. OrderId: {OrderId}", request.OrderId);
throw new NotFoundException(nameof(UserOrder), request.OrderId);
}
// اگر قبلاً با موفقیت پرداخت شده، پاسخ idempotent برگردانیم
if (order.PaymentStatus == PaymentStatus.Success && order.TransactionId.HasValue)
{
var existingWallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == order.UserId, cancellationToken);
var existingTransaction = await _context.Transactions
.FirstOrDefaultAsync(t => t.Id == order.TransactionId.Value, cancellationToken);
return new VerifyGoldenPackagePurchaseResponseDto
{
Success = true,
Message = "پرداخت قبلاً با موفقیت تایید شده است.",
OrderId = order.Id,
TransactionId = existingTransaction?.Id ?? order.TransactionId.Value,
ReferenceCode = existingTransaction?.RefId ?? string.Empty,
WalletBalance = existingWallet?.Balance ?? 0
};
}
// 3. Verify با درگاه پرداخت
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority,
request.Authority,
cancellationToken);
if (!verifyResult.IsSuccess)
{
_logger.LogWarning(
"Golden package payment verification failed. OrderId: {OrderId}, Message: {Message}",
request.OrderId,
verifyResult.Message);
order.PaymentStatus = PaymentStatus.Reject;
await _context.SaveChangesAsync(cancellationToken);
throw new ValidationException($"تراکنش ناموفق: {verifyResult.Message}");
}
// 4. شارژ کیف پول (Balance فقط طبق سناریوی پکیج)
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == order.UserId, cancellationToken);
if (wallet == null)
{
_logger.LogError("Wallet not found for UserId: {UserId}", order.UserId);
throw new NotFoundException($"کیف پول کاربر با شناسه {order.UserId} یافت نشد");
}
var oldBalance = wallet.Balance;
wallet.Balance += order.Amount;
_logger.LogInformation(
"Charging wallet Balance for user {UserId} from {OldBalance} to {NewBalance}",
order.UserId,
oldBalance,
wallet.Balance);
// 5. ثبت Transaction
var transaction = new Transaction
{
Amount = order.Amount,
Description = $"خرید پکیج طلایی از درگاه - سفارش #{order.Id}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.Now,
RefId = verifyResult.RefId,
Type = TransactionType.DepositIpg
};
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(cancellationToken);
// 6. ثبت لاگ تغییر کیف پول
var changeLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = order.Amount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = 0,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(changeLog, cancellationToken);
// 7. به‌روزرسانی سفارش و کاربر
order.TransactionId = transaction.Id;
order.PaymentStatus = PaymentStatus.Success;
order.PaymentDate = DateTime.Now;
order.PaymentMethod = PaymentMethod.IPG;
order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Golden package purchase verified successfully. OrderId: {OrderId}, UserId: {UserId}, TransactionId: {TransactionId}, RefId: {RefId}",
order.Id,
order.UserId,
transaction.Id,
verifyResult.RefId);
return new VerifyGoldenPackagePurchaseResponseDto
{
Success = true,
Message = "پرداخت با موفقیت تایید شد. کیف پول شما شارژ گردید.",
OrderId = order.Id,
TransactionId = transaction.Id,
ReferenceCode = verifyResult.RefId,
WalletBalance = wallet.Balance
};
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in VerifyGoldenPackagePurchaseCommand. OrderId: {OrderId}",
request.OrderId);
throw;
}
}
}
@@ -1,23 +0,0 @@
using FluentValidation;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
public class VerifyGoldenPackagePurchaseCommandValidator : AbstractValidator<VerifyGoldenPackagePurchaseCommand>
{
public VerifyGoldenPackagePurchaseCommandValidator()
{
RuleFor(x => x.OrderId)
.GreaterThan(0)
.WithMessage("شناسه سفارش باید بزرگتر از 0 باشد");
RuleFor(x => x.Authority)
.NotEmpty()
.WithMessage("کد Authority الزامی است");
RuleFor(x => x.Status)
.NotEmpty()
.WithMessage("وضعیت پرداخت الزامی است")
.Must(s => s == "OK" || s == "NOK")
.WithMessage("وضعیت باید OK یا NOK باشد");
}
}
@@ -99,9 +99,11 @@ public class VerifyPackagePurchaseCommandHandler
wallet.Balance
);
// شارژ DiscountBalance (موجودی تخفیف) — دو برابر مبلغ سفارش
// شارژ DiscountBalance (موجودی تخفیف) — ضریب تخفیف از پکیج
var oldDiscountBalance = wallet.DiscountBalance;
wallet.DiscountBalance += order.Amount * 2;
var discountMultiplier = order.Package?.DiscountMultiplier ?? 2.0m;
var discountAmount = (long)(order.Amount * discountMultiplier);
wallet.DiscountBalance += discountAmount;
_logger.LogInformation(
"Charging DiscountBalance for UserId {UserId}: {OldBalance} -> {NewBalance}",
@@ -132,7 +134,7 @@ public class VerifyPackagePurchaseCommandHandler
ChangeValue = order.Amount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance - (order.Amount * 2), // قبل از شارژ DiscountBalance
CurrentDiscountBalance = wallet.DiscountBalance - discountAmount, // قبل از شارژ DiscountBalance
ChangeDiscountValue = 0,
IsIncrease = true,
RefrenceId = transaction.Id
@@ -148,19 +150,39 @@ public class VerifyPackagePurchaseCommandHandler
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = order.Amount * 2,
ChangeDiscountValue = discountAmount,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(discountLog, cancellationToken);
// 8. به‌روزرسانی Order
// 8. ثبت UserPackagePurchase
if (order.PackageId.HasValue)
{
var packagePurchase = new UserPackagePurchase
{
UserId = order.UserId,
PackageId = order.PackageId.Value,
PurchaseMethod = PackagePurchaseMethod.DirectPurchase,
PurchasedAt = DateTime.Now,
Amount = order.Amount,
OrderId = order.Id,
TransactionId = transaction.Id
};
await _context.UserPackagePurchases.AddAsync(packagePurchase, cancellationToken);
_logger.LogInformation(
"Created UserPackagePurchase for UserId {UserId}, PackageId {PackageId}",
order.UserId, order.PackageId.Value);
}
// 9. به‌روزرسانی Order
order.TransactionId = transaction.Id;
order.PaymentStatus = PaymentStatus.Success;
order.PaymentDate = DateTime.Now;
order.PaymentMethod = PaymentMethod.IPG;
// 9. تغییر User.PackagePurchaseMethod
// 10. تغییر User.PackagePurchaseMethod
order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
await _context.SaveChangesAsync(cancellationToken);
@@ -11,8 +11,10 @@ public class GetAllPackageByFilterQueryHandler : IRequestHandler<GetAllPackageBy
public async Task<GetAllPackageByFilterResponseDto> Handle(GetAllPackageByFilterQuery request, CancellationToken cancellationToken)
{
var query = _context.Packages
.Where(x => !x.IsDeleted)
.ApplyOrder(sortBy: request.SortBy)
.AsNoTracking()
.Include(x => x.PackageFeatures)
.AsQueryable();
if (request.Filter is not null)
{
@@ -24,11 +26,24 @@ public class GetAllPackageByFilterQueryHandler : IRequestHandler<GetAllPackageBy
.Where(x => request.Filter.Price == null || x.Price == request.Filter.Price)
;
}
var packages = await query
.PaginatedListAsync(paginationState: request.PaginationState)
.ToListAsync(cancellationToken);
var models = packages.Select(p =>
{
var model = p.Adapt<GetAllPackageByFilterResponseModel>();
model.FeatureIds = p.PackageFeatures?
.Where(pf => pf.IsIncluded)
.Select(pf => pf.ClubFeatureId)
.ToList() ?? new();
return model;
}).ToList();
return new GetAllPackageByFilterResponseDto
{
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
.ProjectToType<GetAllPackageByFilterResponseModel>().ToListAsync(cancellationToken)
Models = models
};
}
}
@@ -18,4 +18,18 @@ public class GetAllPackageByFilterResponseDto
public string ImagePath { get; set; }
//قیمت
public long Price { get; set; }
// فیلدهای جدید پکیج
public int SortOrder { get; set; }
public bool IsActive { get; set; }
public bool IsBasePackage { get; set; }
public bool SupportsDayaPurchase { get; set; }
public bool SupportsDirectPurchase { get; set; }
public long ActivationFee { get; set; }
public decimal DiscountMultiplier { get; set; }
public decimal MagicWalletMultiplier { get; set; }
public int MaxBalancesPerLeg { get; set; }
public int MaxNetworkLevel { get; set; }
public long MagicWalletMaxDeposit { get; set; }
public long MagicWalletMaxCredit { get; set; }
public List<long> FeatureIds { get; set; } = new();
}
@@ -18,44 +18,60 @@ public class GetCustomerPackageDetailsQueryHandler : IRequestHandler<GetCustomer
{
var package = await _context.Packages
.AsNoTracking()
.Where(x => x.Id == request.PackageId)
.ProjectToType<GetCustomerPackageDetailsResponseDto>()
.FirstOrDefaultAsync(cancellationToken);
.Include(p => p.PackageFeatures)
.ThenInclude(pf => pf.ClubFeature)
.FirstOrDefaultAsync(x => x.Id == request.PackageId && !x.IsDeleted, cancellationToken);
if (package == null)
throw new NotFoundException(nameof(Package), request.PackageId);
// Add features based on package (this could be stored in DB in future)
package.Features = new List<PackageFeatureDto>
var response = new GetCustomerPackageDetailsResponseDto
{
new PackageFeatureDto
{
Title = "درآمد کمیسیون",
Description = "دریافت کمیسیون از فروش محصولات",
Icon = "commission",
IsHighlighted = true
},
new PackageFeatureDto
{
Title = "پشتیبانی 24/7",
Description = "دسترسی به پشتیبانی در تمام ساعات شبانه روز",
Icon = "support",
IsHighlighted = false
},
new PackageFeatureDto
{
Title = "آموزش‌های تخصصی",
Description = "دسترسی به دوره‌های آموزشی و وبینارها",
Icon = "education",
IsHighlighted = true
}
Id = package.Id,
Title = package.Title,
Description = package.Description,
Price = package.Price,
ImagePath = package.ImagePath,
ActivationFee = package.ActivationFee,
DiscountMultiplier = (double)package.DiscountMultiplier,
MagicWalletMultiplier = (double)package.MagicWalletMultiplier,
MagicWalletMaxDeposit = package.MagicWalletMaxDeposit,
MagicWalletMaxCredit = package.MagicWalletMaxCredit,
IsBasePackage = package.IsBasePackage,
SupportsDayaPurchase = package.SupportsDayaPurchase,
SupportsDirectPurchase = package.SupportsDirectPurchase
};
// Set purchase requirements
package.Requirements = new PurchaseRequirementsDto
// بارگذاری ویژگی‌ها از DB (PackageFeatures → ClubFeature)
if (package.PackageFeatures?.Any() == true)
{
response.Features = package.PackageFeatures
.Where(pf => pf.ClubFeature != null)
.Select(pf => new PackageFeatureDto
{
Title = pf.ClubFeature.Title ?? string.Empty,
Description = pf.ClubFeature.Description ?? string.Empty,
Icon = "feature",
IsHighlighted = pf.ClubFeature.IsActive
})
.ToList();
}
else
{
// فال‌بک به ویژگی‌های پیش‌فرض
response.Features = new List<PackageFeatureDto>
{
new() { Title = "درآمد کمیسیون", Description = "دریافت کمیسیون از فروش محصولات", Icon = "commission", IsHighlighted = true },
new() { Title = "پشتیبانی 24/7", Description = "دسترسی به پشتیبانی در تمام ساعات شبانه روز", Icon = "support", IsHighlighted = false },
new() { Title = "آموزش‌های تخصصی", Description = "دسترسی به دوره‌های آموزشی و وبینارها", Icon = "education", IsHighlighted = true }
};
}
// شرایط خرید
response.Requirements = new PurchaseRequirementsDto
{
RequiresMembership = false,
MinimumWalletBalance = package.Price / 10, // 10% minimum
MinimumWalletBalance = package.Price / 10,
Restrictions = new List<string>
{
"باید حداقل 18 سال سن داشته باشید",
@@ -63,6 +79,6 @@ public class GetCustomerPackageDetailsQueryHandler : IRequestHandler<GetCustomer
}
};
return package;
return response;
}
}
@@ -9,6 +9,15 @@ public class GetCustomerPackageDetailsResponseDto
public string ImagePath { get; set; }
public List<PackageFeatureDto> Features { get; set; } = new();
public PurchaseRequirementsDto Requirements { get; set; }
// New package-based fields
public long ActivationFee { get; set; }
public double DiscountMultiplier { get; set; }
public double MagicWalletMultiplier { get; set; }
public long MagicWalletMaxDeposit { get; set; }
public long MagicWalletMaxCredit { get; set; }
public bool IsBasePackage { get; set; }
public bool SupportsDayaPurchase { get; set; }
public bool SupportsDirectPurchase { get; set; }
}
public class PackageFeatureDto
@@ -17,35 +17,43 @@ public class GetCustomerPackagesQueryHandler : IRequestHandler<GetCustomerPackag
{
var query = _context.Packages
.AsNoTracking()
.Where(p => !p.IsDeleted)
.AsQueryable();
// Filter by PackageType if specified
if (request.PackageTypeFilter.HasValue)
// فیلتر IsActive
if (!request.IncludeInactive)
query = query.Where(p => p.IsActive);
// مرتب‌سازی بر اساس SortOrder
query = query.OrderBy(p => p.SortOrder);
var packages = await query.ToListAsync(cancellationToken);
return packages.Select(p => new GetCustomerPackagesResponseDto
{
// Note: Package entity doesn't have PackageType enum, so we filter by convention
// Assuming Title or Description contains the package type indicator
// If Package entity needs PackageType field, it should be added to migration
}
// Get all packages (assuming all are available unless marked otherwise)
var packages = await query
.ProjectToType<GetCustomerPackagesResponseDto>()
.ToListAsync(cancellationToken);
// Map additional fields
foreach (var package in packages)
{
package.Name = package.Title;
package.ImageUrl = package.ImagePath;
package.Currency = "IRR";
package.IsAvailable = true;
package.ValidityDays = 365; // Default validity
package.IsPopular = false;
package.ShortDescription = package.Description?.Length > 100
? package.Description.Substring(0, 100) + "..."
: package.Description;
}
return packages;
Id = p.Id,
Name = p.Title,
Title = p.Title,
Description = p.Description,
Price = p.Price,
ImageUrl = p.ImagePath,
ImagePath = p.ImagePath,
Currency = "IRR",
IsAvailable = p.IsActive,
ValidityDays = 365,
IsPopular = p.IsBasePackage,
ShortDescription = p.Description?.Length > 100
? p.Description.Substring(0, 100) + "..."
: p.Description,
// New fields
ActivationFee = p.ActivationFee,
DiscountMultiplier = (double)p.DiscountMultiplier,
MagicWalletMultiplier = (double)p.MagicWalletMultiplier,
MagicWalletMaxDeposit = p.MagicWalletMaxDeposit,
MagicWalletMaxCredit = p.MagicWalletMaxCredit,
IsBasePackage = p.IsBasePackage,
SupportsDayaPurchase = p.SupportsDayaPurchase,
SupportsDirectPurchase = p.SupportsDirectPurchase
}).ToList();
}
}
@@ -15,4 +15,13 @@ public class GetCustomerPackagesResponseDto
public int ValidityDays { get; set; }
public bool IsPopular { get; set; }
public string ShortDescription { get; set; }
// New package-based fields
public long ActivationFee { get; set; }
public double DiscountMultiplier { get; set; }
public double MagicWalletMultiplier { get; set; }
public long MagicWalletMaxDeposit { get; set; }
public long MagicWalletMaxCredit { get; set; }
public bool IsBasePackage { get; set; }
public bool SupportsDayaPurchase { get; set; }
public bool SupportsDirectPurchase { get; set; }
}
@@ -33,6 +33,7 @@ public class GetCustomerPurchaseHistoryQueryHandler : IRequestHandler<GetCustome
.AsNoTracking()
.Where(x => x.UserId == userId && x.PackageId != null)
.Include(x => x.Package)
.Include(x => x.Transaction)
.AsQueryable();
// Apply date filters if specified
@@ -11,12 +11,19 @@ public class GetPackageQueryHandler : IRequestHandler<GetPackageQuery, GetPackag
public async Task<GetPackageResponseDto> Handle(GetPackageQuery request,
CancellationToken cancellationToken)
{
var response = await _context.Packages
var package = await _context.Packages
.AsNoTracking()
.Include(x => x.PackageFeatures)
.Where(x => x.Id == request.Id)
.ProjectToType<GetPackageResponseDto>()
.FirstOrDefaultAsync(cancellationToken);
.FirstOrDefaultAsync(cancellationToken)
?? throw new NotFoundException(nameof(Package), request.Id);
return response ?? throw new NotFoundException(nameof(Package), request.Id);
var response = package.Adapt<GetPackageResponseDto>();
response.FeatureIds = package.PackageFeatures?
.Where(pf => pf.IsIncluded)
.Select(pf => pf.ClubFeatureId)
.ToList() ?? new();
return response;
}
}
@@ -11,5 +11,18 @@ public class GetPackageResponseDto
public string ImagePath { get; set; }
//قیمت
public long Price { get; set; }
// فیلدهای جدید پکیج
public int SortOrder { get; set; }
public bool IsActive { get; set; }
public bool IsBasePackage { get; set; }
public bool SupportsDayaPurchase { get; set; }
public bool SupportsDirectPurchase { get; set; }
public long ActivationFee { get; set; }
public decimal DiscountMultiplier { get; set; }
public decimal MagicWalletMultiplier { get; set; }
public int MaxBalancesPerLeg { get; set; }
public int MaxNetworkLevel { get; set; }
public long MagicWalletMaxDeposit { get; set; }
public long MagicWalletMaxCredit { get; set; }
public List<long> FeatureIds { get; set; } = new();
}
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Enums;
using MediatR;
@@ -15,47 +16,49 @@ public class GetUserPackageStatusQueryHandler : IRequestHandler<GetUserPackageSt
public async Task<UserPackageStatusDto> Handle(GetUserPackageStatusQuery request, CancellationToken cancellationToken)
{
// TODO: پیاده‌سازی دریافت وضعیت پکیج کاربر
//
// 1. دریافت اطلاعات کاربر:
// - var user = await _context.Users
// .Include(u => u.UserWallet)
// .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken)
// - if (user == null) throw new NotFoundException("کاربر یافت نشد")
//
// 2. دریافت عضویت باشگاه:
// - var clubMembership = await _context.ClubMemberships
// .FirstOrDefaultAsync(c => c.UserId == user.Id && c.IsActive, cancellationToken)
//
// 3. دریافت آخرین سفارش پکیج:
// - var lastPackageOrder = await _context.UserOrders
// .Where(o => o.UserId == user.Id && o.PackageId != null)
// .OrderByDescending(o => o.Created)
// .FirstOrDefaultAsync(cancellationToken)
//
// 4. بررسی شرایط فعالسازی باشگاه:
// - var wallet = user.UserWallet
// - bool canActivate =
// user.PackagePurchaseMethod != PackagePurchaseMethod.None &&
// clubMembership == null &&
// wallet != null &&
// wallet.Balance >= 56_000_000
//
// 5. برگشت DTO:
// - return new UserPackageStatusDto {
// UserId = user.Id,
// PackagePurchaseMethod = user.PackagePurchaseMethod.ToString(),
// HasPurchasedPackage = user.PackagePurchaseMethod != PackagePurchaseMethod.None,
// IsClubMemberActive = clubMembership != null,
// WalletBalance = wallet?.Balance ?? 0,
// DiscountBalance = wallet?.DiscountBalance ?? 0,
// CanActivateClubMembership = canActivate,
// LastOrderNumber = lastPackageOrder?.OrderNumber,
// LastPurchaseDate = lastPackageOrder?.Created
// }
//
// نکته: این query برای UI مفید است تا وضعیت کاربر را نمایش دهد
// 1. دریافت اطلاعات کاربر
var user = await _context.Users
.Include(u => u.UserWallets)
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
throw new NotImplementedException("GetUserPackageStatus needs implementation");
if (user == null)
throw new NotFoundException(nameof(User), request.UserId);
// 2. دریافت عضویت باشگاه
var clubMembership = await _context.ClubMemberships
.FirstOrDefaultAsync(c => c.UserId == user.Id && c.IsActive, cancellationToken);
// 3. دریافت آخرین سفارش پکیج
var lastPackageOrder = await _context.UserOrders
.Where(o => o.UserId == user.Id && o.PackageId != null)
.OrderByDescending(o => o.Created)
.FirstOrDefaultAsync(cancellationToken);
// 4. بارگذاری پکیج پایه برای بررسی شرایط فعالسازی
var basePackage = await _context.Packages
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken);
var wallet = user.UserWallets.FirstOrDefault();
// 5. بررسی شرایط فعالسازی باشگاه
var canActivate =
user.PackagePurchaseMethod != PackagePurchaseMethod.None &&
clubMembership == null &&
wallet != null &&
basePackage != null &&
wallet.Balance >= basePackage.Price;
return new UserPackageStatusDto
{
UserId = user.Id,
PackagePurchaseMethod = user.PackagePurchaseMethod.ToString(),
HasPurchasedPackage = user.PackagePurchaseMethod != PackagePurchaseMethod.None,
IsClubMemberActive = clubMembership?.IsActive ?? false,
WalletBalance = wallet?.Balance ?? 0,
DiscountBalance = wallet?.DiscountBalance ?? 0,
CanActivateClubMembership = canActivate,
LastOrderNumber = lastPackageOrder?.Id.ToString(),
LastPurchaseDate = lastPackageOrder?.Created
};
}
}
@@ -76,8 +76,17 @@ public class ChargeMagicWalletCommandHandler
);
}
// 3.5. بارگذاری پکیج کاربر برای سقف کیف‌پول جادویی
var currentCycle = await _context.ClubMembershipCycles
.FirstOrDefaultAsync(c => c.UserId == request.UserId && c.IsCurrentCycle, cancellationToken);
var package = currentCycle != null
? await _context.Packages.FirstOrDefaultAsync(p => p.Id == currentCycle.PackageId, cancellationToken)
: await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken);
if (package == null)
throw new NotFoundException("پکیج یافت نشد");
// 4. بررسی سقف واریزی (per-cycle)
var remainingDeposit = SystemConstants.MagicWalletMaxDeposit - wallet.MagicTotalDeposited;
var remainingDeposit = package.MagicWalletMaxDeposit - wallet.MagicTotalDeposited;
if (remainingDeposit <= 0)
{
@@ -121,8 +121,17 @@ public class VerifyMagicWalletChargeCommandHandler
throw new BadRequestException("کیف‌پول در حالت جادویی نیست");
}
// 5. محاسبه اعتبار ×2.5
var creditAmount = (long)(depositAmount * SystemConstants.MagicWalletMultiplier); // مبلغ × 2.5
// 4.5. بارگذاری پکیج کاربر برای ضریب جادویی
var currentCycle = await _context.ClubMembershipCycles
.FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, cancellationToken);
var package = currentCycle != null
? await _context.Packages.FirstOrDefaultAsync(p => p.Id == currentCycle.PackageId, cancellationToken)
: await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken);
if (package == null)
throw new NotFoundException("پکیج یافت نشد");
// 5. محاسبه اعتبار
var creditAmount = (long)(depositAmount * package.MagicWalletMultiplier); // مبلغ × 2.5
var bonusAmount = creditAmount - depositAmount; // بونوس = مبلغ × 1.5
// 6. ثبت تراکنش واریز واقعی (MagicWalletDeposit)
@@ -143,7 +152,7 @@ public class VerifyMagicWalletChargeCommandHandler
var bonusTransaction = new Transaction
{
Amount = bonusAmount,
Description = $"شارژ کیف‌پول جادویی - بونوس ×{SystemConstants.MagicWalletMultiplier - 1} - کاربر {userId}",
Description = $"شارژ کیف‌پول جادویی - بونوس ×{package.MagicWalletMultiplier - 1} - کاربر {userId}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow,
RefId = $"MAGIC_BONUS_{depositTransaction.Id}",
@@ -186,10 +195,10 @@ public class VerifyMagicWalletChargeCommandHandler
userId,
depositAmount,
creditAmount,
SystemConstants.MagicWalletMultiplier,
package.MagicWalletMultiplier,
bonusAmount,
wallet.MagicTotalDeposited,
SystemConstants.MagicWalletMaxDeposit,
package.MagicWalletMaxDeposit,
wallet.Balance
);
@@ -24,12 +24,16 @@ public static class SystemConstants
/// <summary>
/// مبلغ هدیه حق عضویت باشگاه (ریال) - این مبلغ از کیف پول کم نمی‌شود
/// [DEPRECATED] از Package.Price استفاده کنید
/// </summary>
[Obsolete("Moved to Package entity. Use package.Price or package.ActivationFee instead.")]
public const long ClubMembershipGiftValue = 25_200_000;
/// <summary>
/// هزینه فعال‌سازی عضویت باشگاه (ریال)
/// [DEPRECATED] از Package.ActivationFee استفاده کنید
/// </summary>
[Obsolete("Moved to Package.ActivationFee. Read from Package entity instead.")]
public const long ClubActivationFee = 25_200_000;
#endregion
@@ -37,14 +41,17 @@ public static class SystemConstants
#region Package Settings
/// <summary>
/// مبلغ پکیج طلایی / پایه (ریال) - 56 میلیون تومان
/// شامل: هدیه باشگاه + هزینه فعال‌سازی + مزایای دیگر
/// مبلغ پکیج پایه (ریال)
/// [DEPRECATED] از Package.Price استفاده کنید
/// </summary>
[Obsolete("Moved to Package.Price. Read from Package entity instead.")]
public const long BasePackageAmount = 56_000_000;
/// <summary>
/// مبلغ وام دایا (ریال) - همان مبلغ پکیج طلایی
/// مبلغ وام دایا (ریال)
/// [DEPRECATED] از Package.Price استفاده کنید
/// </summary>
[Obsolete("Moved to Package.Price. Read from Package entity with SupportsDayaPurchase=true.")]
public const long DayaLoanAmount = 56_000_000;
#endregion
@@ -63,12 +70,16 @@ public static class SystemConstants
/// <summary>
/// سقف تعادل هفتگی برای هر دست (چپ یا راست) - حداکثر کل = 600
/// [DEPRECATED] از Package.MaxBalancesPerLeg استفاده کنید
/// </summary>
[Obsolete("Moved to Package.MaxBalancesPerLeg. Read from Package entity instead.")]
public const int CommissionMaxWeeklyBalancesPerLeg = 300;
/// <summary>
/// حداکثر عمق شبکه برای محاسبه کمیسیون (تعداد لول زیرمجموعه)
/// [DEPRECATED] از Package.MaxNetworkLevel استفاده کنید
/// </summary>
[Obsolete("Moved to Package.MaxNetworkLevel. Read from Package entity instead.")]
public const int CommissionMaxNetworkLevel = 15;
/// <summary>
@@ -96,17 +107,23 @@ public static class SystemConstants
/// <summary>
/// ضریب شارژ کیف‌پول جادویی — واریز × 2.5 = اعتبار
/// [DEPRECATED] از Package.MagicWalletMultiplier استفاده کنید
/// </summary>
[Obsolete("Moved to Package.MagicWalletMultiplier. Read from Package entity instead.")]
public const decimal MagicWalletMultiplier = 2.5m;
/// <summary>
/// سقف واریز در هر دور جادویی (ریال) — 100M تومان
/// [DEPRECATED] از Package.MagicWalletMaxDeposit استفاده کنید
/// </summary>
[Obsolete("Moved to Package.MagicWalletMaxDeposit. Read from Package entity instead.")]
public const long MagicWalletMaxDeposit = 1_000_000_000;
/// <summary>
/// سقف اعتبار در هر دور جادویی (ریال) — 250M تومان
/// [DEPRECATED] از Package.MagicWalletMaxCredit استفاده کنید
/// </summary>
[Obsolete("Moved to Package.MagicWalletMaxCredit. Read from Package entity instead.")]
public const long MagicWalletMaxCredit = 2_500_000_000;
#endregion
@@ -129,6 +146,8 @@ public static class SystemConstants
/// <summary>
/// دریافت مقدار به صورت دیکشنری برای نمایش در Admin Panel
/// Note: Per-package values (Price, MaxBalancesPerLeg, etc.) are now read from Package entity
/// via ConfigurationService — only static/global constants remain here.
/// </summary>
public static Dictionary<string, object> GetAllAsDict()
{
@@ -138,26 +157,15 @@ public static class SystemConstants
["Network.AllowOrphanNodes"] = NetworkAllowOrphanNodes,
["Network.MaxChildrenPerLeg"] = NetworkMaxChildrenPerLeg,
// Club
["Club.MembershipGiftValue"] = ClubMembershipGiftValue,
["Club.ActivationFee"] = ClubActivationFee,
// Commission
// Commission (global settings only — per-package limits are in Package entity)
["Commission.CashWithdrawalEnabled"] = CommissionCashWithdrawalEnabled,
["Commission.MinWithdrawalAmount"] = CommissionMinWithdrawalAmount,
["Commission.MaxWeeklyBalancesPerLeg"] = CommissionMaxWeeklyBalancesPerLeg,
["Commission.MaxNetworkLevel"] = CommissionMaxNetworkLevel,
["Commission.CalculationStrategy"] = CommissionCalculationStrategy,
// System
["System.MaintenanceMode"] = SystemMaintenanceMode,
["System.EnableAuditLog"] = SystemEnableAuditLog,
// Magic Wallet
["MagicWallet.Multiplier"] = MagicWalletMultiplier,
["MagicWallet.MaxDeposit"] = MagicWalletMaxDeposit,
["MagicWallet.MaxCredit"] = MagicWalletMaxCredit,
// Shop
["Shop.VAT"] = ShopVAT,
["Shop.VATEnabled"] = ShopVATEnabled
@@ -166,6 +174,7 @@ public static class SystemConstants
/// <summary>
/// دریافت لیست تنظیمات با توضیحات
/// Note: Per-package values are served from Package entity via ConfigurationService.
/// </summary>
public static List<(string Key, object Value, string Description)> GetAllWithDescriptions()
{
@@ -175,26 +184,15 @@ public static class SystemConstants
("Network.AllowOrphanNodes", NetworkAllowOrphanNodes, "اجازه حذف والدین که فرزند دارند"),
("Network.MaxChildrenPerLeg", NetworkMaxChildrenPerLeg, "حداکثر تعداد فرزند مستقیم در هر پا"),
// Club
("Club.MembershipGiftValue", ClubMembershipGiftValue, "مبلغ هدیه حق عضویت باشگاه (ریال)"),
("Club.ActivationFee", ClubActivationFee, "هزینه فعال‌سازی عضویت باشگاه (ریال)"),
// Commission
// Commission (global settings only)
("Commission.CashWithdrawalEnabled", CommissionCashWithdrawalEnabled, "امکان برداشت نقدی فعال باشد"),
("Commission.MinWithdrawalAmount", CommissionMinWithdrawalAmount, "حداقل مبلغ برداشت (ریال)"),
("Commission.MaxWeeklyBalancesPerLeg", CommissionMaxWeeklyBalancesPerLeg, "سقف تعادل هفتگی برای هر دست"),
("Commission.MaxNetworkLevel", CommissionMaxNetworkLevel, "حداکثر عمق شبکه برای محاسبه کمیسیون"),
("Commission.CalculationStrategy", CommissionCalculationStrategy, "روش محاسبه (ORM/SP)"),
// System
("System.MaintenanceMode", SystemMaintenanceMode, "حالت تعمیر و نگهداری سیستم"),
("System.EnableAuditLog", SystemEnableAuditLog, "فعال‌سازی لاگ تغییرات"),
// Magic Wallet
("MagicWallet.Multiplier", MagicWalletMultiplier, "ضریب شارژ کیف‌پول جادویی (×2.5)"),
("MagicWallet.MaxDeposit", MagicWalletMaxDeposit, "سقف واریز هر دور جادویی (ریال)"),
("MagicWallet.MaxCredit", MagicWalletMaxCredit, "سقف اعتبار هر دور جادویی (ریال)"),
// Shop
("Shop.VAT", ShopVAT, "مالیات بر ارزش افزوده"),
("Shop.VATEnabled", ShopVATEnabled, "مالیات فعال است؟")
@@ -20,9 +20,45 @@ public class ClubMembership : BaseAuditableEntity
/// </summary>
public bool IsActive { get; set; }
// === v5: First/Last Activation Tracking (Q21) ===
/// <summary>
/// تاریخ فعال‌سازی عضویت
/// اولین فعال‌سازی — فقط یک بار ست می‌شود، هیچ‌وقت overwrite نمی‌شود
/// </summary>
public DateTime? FirstActivationDate { get; set; }
/// <summary>
/// اولین پکیج خریداری‌شده — فقط یک بار ست می‌شود
/// </summary>
public long? FirstPackageId { get; set; }
/// <summary>
/// FirstPackage Navigation Property
/// </summary>
public virtual Package? FirstPackage { get; set; }
/// <summary>
/// آخرین/جاری فعال‌سازی — هر خرید مجدد بروزرسانی می‌شود
/// مبنای تشخیص "فعال‌شدگان این هفته" (Q22)
/// </summary>
public DateTime? LastActivationDate { get; set; }
/// <summary>
/// آخرین/جاری پکیج — هر خرید مجدد بروزرسانی می‌شود
/// مبنای carryover per-package (Q23)
/// </summary>
public long? LastPackageId { get; set; }
/// <summary>
/// LastPackage Navigation Property
/// </summary>
public virtual Package? LastPackage { get; set; }
/// <summary>
/// [DEPRECATED] تاریخ فعال‌سازی — جایگزین با FirstActivationDate + LastActivationDate
/// حفظ موقت برای backward compatibility
/// </summary>
[Obsolete("Use FirstActivationDate / LastActivationDate instead")]
public DateTime? ActivatedAt { get; set; }
/// <summary>
@@ -57,6 +57,16 @@ public class ClubMembershipCycle : BaseAuditableEntity
/// </summary>
public long PackageAmount { get; set; }
/// <summary>
/// شناسه پکیج خریداری‌شده در این دور
/// </summary>
public long PackageId { get; set; }
/// <summary>
/// Package Navigation Property
/// </summary>
public virtual Package Package { get; set; }
/// <summary>
/// آیا این دور فعلی است؟ فقط یک رکورد true می‌باشد
/// </summary>
@@ -36,6 +36,16 @@ public class UserCommissionPayout : BaseAuditableEntity
/// </summary>
public virtual WeeklyCommissionPool WeeklyPool { get; set; }
/// <summary>
/// شناسه پکیج (کمیسیون هر پکیج جداگانه)
/// </summary>
public long PackageId { get; set; }
/// <summary>
/// Package Navigation Property
/// </summary>
public virtual Package Package { get; set; }
/// <summary>
/// تعداد امتیازی که کاربر داشت
/// </summary>
@@ -16,6 +16,16 @@ public class WeeklyCommissionPool : BaseAuditableEntity
/// </summary>
public virtual WeekDefinition WeekDefinition { get; set; }
/// <summary>
/// شناسه پکیج (استخر هر پکیج جداگانه)
/// </summary>
public long PackageId { get; set; }
/// <summary>
/// Package Navigation Property
/// </summary>
public virtual Package Package { get; set; }
/// <summary>
/// مجموع مبلغ جمع‌شده در استخر (ریال)
/// </summary>
@@ -25,6 +25,16 @@ public class NetworkWeeklyBalance : BaseAuditableEntity
/// </summary>
public virtual WeekDefinition WeekDefinition { get; set; }
/// <summary>
/// شناسه پکیج (تعادل هر پکیج جداگانه)
/// </summary>
public long PackageId { get; set; }
/// <summary>
/// Package Navigation Property
/// </summary>
public virtual Package Package { get; set; }
/// <summary>
/// تعداد اعضای جدید شاخه چپ در این هفته
/// </summary>
+67 -6
View File
@@ -1,15 +1,76 @@
namespace CMSMicroservice.Domain.Entities;
//پکیج
/// <summary>
/// پکیج — هر پکیج قیمت، ویژگی‌ها و تنظیمات مستقل دارد
/// </summary>
public class Package : BaseAuditableEntity
{
//عنوان
// === فیلدهای فعلی (حفظ) ===
/// <summary>عنوان پکیج</summary>
public string Title { get; set; }
//توضیحات
/// <summary>توضیحات</summary>
public string Description { get; set; }
//آدرس تصویر
/// <summary>آدرس تصویر</summary>
public string ImagePath { get; set; }
//قیمت
/// <summary>قیمت پکیج (ریال)</summary>
public long Price { get; set; }
//UserOrder Collection Navigation Reference
// === فیلدهای جدید v3 ===
/// <summary>ترتیب نمایش</summary>
public int SortOrder { get; set; }
/// <summary>فعال/غیرفعال</summary>
public bool IsActive { get; set; } = true;
/// <summary>پکیج پایه؟ (فقط یکی true)</summary>
public bool IsBasePackage { get; set; }
/// <summary>پشتیبانی از خرید دایا</summary>
public bool SupportsDayaPurchase { get; set; }
/// <summary>پشتیبانی از پرداخت مستقیم</summary>
public bool SupportsDirectPurchase { get; set; } = true;
// === محاسبات مالی ===
/// <summary>سهم Commission Pool (ریال) — معمولاً Price × 0.45</summary>
public long ActivationFee { get; set; }
/// <summary>ضریب شارژ DiscountBalance — فعلاً ×2 برای همه</summary>
public decimal DiscountMultiplier { get; set; } = 2.0m;
/// <summary>ضریب کیف‌پول جادویی — واریز × این مقدار = اعتبار</summary>
public decimal MagicWalletMultiplier { get; set; } = 2.5m;
// === تنظیمات پورسانت ===
/// <summary>سقف تعادل هر پا (نقره‌ای=۳۰، پایه=۳۰۰)</summary>
public int MaxBalancesPerLeg { get; set; } = 300;
/// <summary>عمق شبکه برای محاسبه کمیسیون</summary>
public int MaxNetworkLevel { get; set; } = 15;
// === تنظیمات کیف‌پول جادویی ===
/// <summary>سقف شارژ هر دور جادویی (ریال)</summary>
public long MagicWalletMaxDeposit { get; set; } = 1_000_000_000;
/// <summary>سقف اعتبار هر دور جادویی (ریال)</summary>
public long MagicWalletMaxCredit { get; set; } = 2_500_000_000;
// === Navigation Properties ===
/// <summary>فیچرهای این پکیج</summary>
public virtual ICollection<PackageFeature>? PackageFeatures { get; set; }
/// <summary>سفارشات مرتبط</summary>
public virtual ICollection<UserOrder> UserOrders { get; set; }
/// <summary>خریدهای پکیج</summary>
public virtual ICollection<UserPackagePurchase>? Purchases { get; set; }
}
@@ -0,0 +1,34 @@
using CMSMicroservice.Domain.Entities.Club;
namespace CMSMicroservice.Domain.Entities;
/// <summary>
/// ارتباط پکیج–فیچر — هر پکیج مجموعه فیچرهای خودش را دارد
/// </summary>
public class PackageFeature : BaseAuditableEntity
{
/// <summary>
/// شناسه پکیج
/// </summary>
public long PackageId { get; set; }
/// <summary>
/// Package Navigation Property
/// </summary>
public virtual Package Package { get; set; }
/// <summary>
/// شناسه فیچر باشگاه
/// </summary>
public long ClubFeatureId { get; set; }
/// <summary>
/// ClubFeature Navigation Property
/// </summary>
public virtual ClubFeature ClubFeature { get; set; }
/// <summary>
/// آیا این فیچر در پکیج فعال است
/// </summary>
public bool IsIncluded { get; set; } = true;
}
+1 -1
View File
@@ -63,7 +63,7 @@ public class User : BaseAuditableEntity
public DateTime? DayaCreditReceivedAt { get; set; }
/// <summary>
/// نحوه خرید پکیج طلایی (برای جلوگیری از خرید مجدد)
/// نحوه خرید پکیج (برای جلوگیری از خرید مجدد)
/// </summary>
public PackagePurchaseMethod PackagePurchaseMethod { get; set; } = PackagePurchaseMethod.None;
@@ -10,7 +10,7 @@ public class UserWallet : BaseAuditableEntity
public long Balance { get; set; }
/// <summary>
/// موجودی شبکه/کارمزد (کیف پول طلایی) - قابل برداشت نقدی یا خرید الماس
/// موجودی شبکه/کارمزد قابل برداشت نقدی یا خرید الماس
/// </summary>
public long NetworkBalance { get; set; }
@@ -22,4 +22,14 @@ public class UserWalletChangeLog : BaseAuditableEntity
public bool IsIncrease { get; set; }
//شناسه ارجاع
public long? RefrenceId { get; set; }
/// <summary>
/// شناسه پکیج مرتبط (nullable — برای تغییرات کیف پول که مرتبط با پکیج هستند)
/// </summary>
public long? PackageId { get; set; }
/// <summary>
/// Package Navigation Property
/// </summary>
public virtual Package? Package { get; set; }
}
@@ -11,7 +11,7 @@ public enum CommissionPayoutStatus
Pending = 0,
/// <summary>
/// واریز شده به کیف پول طلایی
/// واریز شده به کیف پول
/// </summary>
Paid = 1,
@@ -1,7 +1,7 @@
namespace CMSMicroservice.Domain.Enums;
/// <summary>
/// نحوه خرید پکیج طلایی توسط کاربر
/// نحوه خرید پکیج توسط کاربر
/// </summary>
public enum PackagePurchaseMethod
{
@@ -110,6 +110,7 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
public DbSet<UserClubFeature> UserClubFeatures => Set<UserClubFeature>();
public DbSet<ClubMembershipHistory> ClubMembershipHistories => Set<ClubMembershipHistory>();
public DbSet<ClubMembershipCycle> ClubMembershipCycles => Set<ClubMembershipCycle>();
public DbSet<PackageFeature> PackageFeatures => Set<PackageFeature>();
// Network
public DbSet<NetworkWeeklyBalance> NetworkWeeklyBalances => Set<NetworkWeeklyBalance>();
@@ -23,12 +23,32 @@ public class ClubMembershipConfiguration : IEntityTypeConfiguration<ClubMembersh
builder.Property(entity => entity.GiftValue).IsRequired();
builder.Property(entity => entity.TotalEarned).IsRequired();
// فیلدهای جدید First/Last Activation
builder.Property(entity => entity.FirstActivationDate).IsRequired(false);
builder.Property(entity => entity.FirstPackageId).IsRequired(false);
builder.Property(entity => entity.LastActivationDate).IsRequired(false);
builder.Property(entity => entity.LastPackageId).IsRequired(false);
// رابطه یک‌به‌یک با User
builder.HasOne(entity => entity.User)
.WithOne(u => u.ClubMembership)
.HasForeignKey<ClubMembership>(entity => entity.UserId)
.OnDelete(DeleteBehavior.Restrict);
// رابطه با Package — اولین پکیج خریداری‌شده
builder.HasOne(entity => entity.FirstPackage)
.WithMany()
.HasForeignKey(entity => entity.FirstPackageId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
// رابطه با Package — آخرین پکیج خریداری‌شده
builder.HasOne(entity => entity.LastPackage)
.WithMany()
.HasForeignKey(entity => entity.LastPackageId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
// Index برای UserId (یونیک برای یک‌به‌یک)
builder.HasIndex(e => e.UserId)
.IsUnique()
@@ -37,5 +57,9 @@ public class ClubMembershipConfiguration : IEntityTypeConfiguration<ClubMembersh
// Index برای IsActive
builder.HasIndex(e => e.IsActive)
.HasDatabaseName("IX_ClubMembership_IsActive");
// Index برای LastActivationDate (تشخیص فعال‌سازی هفتگی Q22)
builder.HasIndex(e => e.LastActivationDate)
.HasDatabaseName("IX_ClubMembership_LastActivationDate");
}
}
@@ -21,6 +21,7 @@ public class ClubMembershipCycleConfiguration : IEntityTypeConfiguration<ClubMem
builder.Property(entity => entity.MagicCompletedAt).IsRequired(false);
builder.Property(entity => entity.PurchaseMethod).IsRequired();
builder.Property(entity => entity.PackageAmount).IsRequired();
builder.Property(entity => entity.PackageId).IsRequired();
builder.Property(entity => entity.IsCurrentCycle)
.IsRequired()
.HasDefaultValue(false);
@@ -36,6 +37,11 @@ public class ClubMembershipCycleConfiguration : IEntityTypeConfiguration<ClubMem
.HasForeignKey(entity => entity.ClubMembershipId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(entity => entity.Package)
.WithMany()
.HasForeignKey(entity => entity.PackageId)
.OnDelete(DeleteBehavior.Restrict);
// Indexes
builder.HasIndex(e => new { e.UserId, e.IsCurrentCycle })
.HasDatabaseName("IX_ClubMembershipCycle_UserId_IsCurrentCycle");
@@ -18,6 +18,7 @@ public class NetworkWeeklyBalanceConfiguration : IEntityTypeConfiguration<Networ
builder.Property(entity => entity.UserId).IsRequired();
builder.Property(entity => entity.WeekDefinitionId).IsRequired();
builder.Property(entity => entity.PackageId).IsRequired();
builder.Property(entity => entity.LeftLegBalances).IsRequired();
builder.Property(entity => entity.RightLegBalances).IsRequired();
builder.Property(entity => entity.TotalBalances).IsRequired();
@@ -37,10 +38,16 @@ public class NetworkWeeklyBalanceConfiguration : IEntityTypeConfiguration<Networ
.HasForeignKey(entity => entity.WeekDefinitionId)
.OnDelete(DeleteBehavior.Restrict);
// Composite Index برای UserId و WeekDefinitionId
builder.HasIndex(e => new { e.UserId, e.WeekDefinitionId })
// رابطه با Package
builder.HasOne(entity => entity.Package)
.WithMany()
.HasForeignKey(entity => entity.PackageId)
.OnDelete(DeleteBehavior.Restrict);
// Composite Index: هر کاربر × هر هفته × هر پکیج فقط یک رکورد
builder.HasIndex(e => new { e.UserId, e.WeekDefinitionId, e.PackageId })
.IsUnique()
.HasDatabaseName("IX_NetworkWeeklyBalance_UserId_WeekDefinitionId");
.HasDatabaseName("IX_NetworkWeeklyBalance_User_WeekDef_Package");
// Index برای WeekDefinitionId
builder.HasIndex(e => e.WeekDefinitionId)
@@ -15,6 +15,28 @@ public class PackageConfiguration : IEntityTypeConfiguration<Package>
builder.Property(entity => entity.Description).IsRequired(true);
builder.Property(entity => entity.ImagePath).IsRequired(true);
builder.Property(entity => entity.Price).IsRequired(true);
// فیلدهای جدید پکیج
builder.Property(entity => entity.SortOrder).IsRequired().HasDefaultValue(0);
builder.Property(entity => entity.IsActive).IsRequired().HasDefaultValue(true);
builder.Property(entity => entity.IsBasePackage).IsRequired().HasDefaultValue(false);
builder.Property(entity => entity.SupportsDayaPurchase).IsRequired().HasDefaultValue(false);
builder.Property(entity => entity.SupportsDirectPurchase).IsRequired().HasDefaultValue(true);
builder.Property(entity => entity.ActivationFee).IsRequired().HasDefaultValue(0L);
builder.Property(entity => entity.DiscountMultiplier).IsRequired().HasDefaultValue(2.0m)
.HasPrecision(18, 4);
builder.Property(entity => entity.MagicWalletMultiplier).IsRequired().HasDefaultValue(2.5m)
.HasPrecision(18, 4);
builder.Property(entity => entity.MaxBalancesPerLeg).IsRequired().HasDefaultValue(300);
builder.Property(entity => entity.MaxNetworkLevel).IsRequired().HasDefaultValue(15);
builder.Property(entity => entity.MagicWalletMaxDeposit).IsRequired().HasDefaultValue(1_000_000_000L);
builder.Property(entity => entity.MagicWalletMaxCredit).IsRequired().HasDefaultValue(2_500_000_000L);
// Indexes
builder.HasIndex(e => e.IsActive)
.HasDatabaseName("IX_Package_IsActive");
builder.HasIndex(e => e.SortOrder)
.HasDatabaseName("IX_Package_SortOrder");
}
}
@@ -0,0 +1,41 @@
using CMSMicroservice.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
/// <summary>
/// EF Configuration — ارتباط پکیج–فیچر
/// </summary>
public class PackageFeatureConfiguration : IEntityTypeConfiguration<PackageFeature>
{
public void Configure(EntityTypeBuilder<PackageFeature> builder)
{
builder.HasQueryFilter(p => !p.IsDeleted);
builder.Ignore(entity => entity.DomainEvents);
builder.HasKey(entity => entity.Id);
builder.Property(entity => entity.Id).UseIdentityColumn();
builder.Property(entity => entity.PackageId).IsRequired();
builder.Property(entity => entity.ClubFeatureId).IsRequired();
builder.Property(entity => entity.IsIncluded).IsRequired().HasDefaultValue(true);
// رابطه با Package
builder.HasOne(entity => entity.Package)
.WithMany(p => p.PackageFeatures)
.HasForeignKey(entity => entity.PackageId)
.OnDelete(DeleteBehavior.Cascade);
// رابطه با ClubFeature
builder.HasOne(entity => entity.ClubFeature)
.WithMany()
.HasForeignKey(entity => entity.ClubFeatureId)
.OnDelete(DeleteBehavior.Restrict);
// Unique: هر فیچر فقط یک بار در هر پکیج
builder.HasIndex(e => new { e.PackageId, e.ClubFeatureId })
.IsUnique()
.HasDatabaseName("IX_PackageFeature_PackageId_ClubFeatureId");
}
}
@@ -19,6 +19,7 @@ public class UserCommissionPayoutConfiguration : IEntityTypeConfiguration<UserCo
builder.Property(entity => entity.UserId).IsRequired();
builder.Property(entity => entity.WeekDefinitionId).IsRequired();
builder.Property(entity => entity.WeeklyPoolId).IsRequired();
builder.Property(entity => entity.PackageId).IsRequired();
builder.Property(entity => entity.BalancesEarned).IsRequired();
builder.Property(entity => entity.ValuePerBalance).IsRequired();
builder.Property(entity => entity.TotalAmount).IsRequired();
@@ -50,10 +51,16 @@ public class UserCommissionPayoutConfiguration : IEntityTypeConfiguration<UserCo
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
// Composite Index برای UserId و WeekDefinitionId
builder.HasIndex(e => new { e.UserId, e.WeekDefinitionId })
// رابطه با Package
builder.HasOne(entity => entity.Package)
.WithMany()
.HasForeignKey(entity => entity.PackageId)
.OnDelete(DeleteBehavior.Restrict);
// Composite Index برای UserId، WeekDefinitionId و PackageId
builder.HasIndex(e => new { e.UserId, e.WeekDefinitionId, e.PackageId })
.IsUnique()
.HasDatabaseName("IX_UserCommissionPayout_UserId_WeekDefinitionId");
.HasDatabaseName("IX_UserCommissionPayout_User_WeekDef_Package");
// Index برای WeeklyPoolId
builder.HasIndex(e => e.WeeklyPoolId)
@@ -33,7 +33,7 @@ public class UserPackagePurchaseConfiguration : IEntityTypeConfiguration<UserPac
// رابطه با Package
builder.HasOne(entity => entity.Package)
.WithMany()
.WithMany(p => p.Purchases)
.HasForeignKey(entity => entity.PackageId)
.OnDelete(DeleteBehavior.Restrict);
@@ -22,6 +22,14 @@ public class UserWalletChangeLogConfiguration : IEntityTypeConfiguration<UserWal
builder.Property(entity => entity.ChangeNerworkValue).IsRequired(true);
builder.Property(entity => entity.IsIncrease).IsRequired(true);
builder.Property(entity => entity.RefrenceId).IsRequired(false);
builder.Property(entity => entity.PackageId).IsRequired(false);
// رابطه با Package (nullable)
builder.HasOne(entity => entity.Package)
.WithMany()
.HasForeignKey(entity => entity.PackageId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -17,6 +17,7 @@ public class WeeklyCommissionPoolConfiguration : IEntityTypeConfiguration<Weekly
builder.Property(entity => entity.Id).UseIdentityColumn();
builder.Property(entity => entity.WeekDefinitionId).IsRequired();
builder.Property(entity => entity.PackageId).IsRequired();
builder.Property(entity => entity.TotalPoolAmount).IsRequired();
builder.Property(entity => entity.TotalBalances).IsRequired();
builder.Property(entity => entity.ValuePerBalance).IsRequired();
@@ -30,10 +31,16 @@ public class WeeklyCommissionPoolConfiguration : IEntityTypeConfiguration<Weekly
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
// Index یونیک برای WeekDefinitionId
builder.HasIndex(e => e.WeekDefinitionId)
// رابطه با Package
builder.HasOne(entity => entity.Package)
.WithMany()
.HasForeignKey(entity => entity.PackageId)
.OnDelete(DeleteBehavior.Restrict);
// Index یونیک ترکیبی: هر هفته × هر پکیج فقط یک استخر
builder.HasIndex(e => new { e.WeekDefinitionId, e.PackageId })
.IsUnique()
.HasDatabaseName("IX_WeeklyCommissionPool_WeekDefinitionId");
.HasDatabaseName("IX_WeeklyCommissionPool_WeekDef_Package");
// Index برای IsCalculated
builder.HasIndex(e => e.IsCalculated)
@@ -0,0 +1,683 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddPackageBasedSystem : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_WeeklyCommissionPool_WeekDefinitionId",
schema: "CMS",
table: "WeeklyCommissionPools");
migrationBuilder.DropIndex(
name: "IX_UserCommissionPayout_UserId_WeekDefinitionId",
schema: "CMS",
table: "UserCommissionPayouts");
migrationBuilder.DropIndex(
name: "IX_NetworkWeeklyBalance_UserId_WeekDefinitionId",
schema: "CMS",
table: "NetworkWeeklyBalances");
migrationBuilder.AddColumn<long>(
name: "PackageId",
schema: "CMS",
table: "WeeklyCommissionPools",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.AddColumn<long>(
name: "PackageId",
schema: "CMS",
table: "UserWalletChangeLogs",
type: "bigint",
nullable: true);
migrationBuilder.AddColumn<long>(
name: "PackageId",
schema: "CMS",
table: "UserCommissionPayouts",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.AddColumn<long>(
name: "ActivationFee",
schema: "CMS",
table: "Packages",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.AddColumn<decimal>(
name: "DiscountMultiplier",
schema: "CMS",
table: "Packages",
type: "decimal(18,4)",
precision: 18,
scale: 4,
nullable: false,
defaultValue: 2.0m);
migrationBuilder.AddColumn<bool>(
name: "IsActive",
schema: "CMS",
table: "Packages",
type: "bit",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<bool>(
name: "IsBasePackage",
schema: "CMS",
table: "Packages",
type: "bit",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<long>(
name: "MagicWalletMaxCredit",
schema: "CMS",
table: "Packages",
type: "bigint",
nullable: false,
defaultValue: 2500000000L);
migrationBuilder.AddColumn<long>(
name: "MagicWalletMaxDeposit",
schema: "CMS",
table: "Packages",
type: "bigint",
nullable: false,
defaultValue: 1000000000L);
migrationBuilder.AddColumn<decimal>(
name: "MagicWalletMultiplier",
schema: "CMS",
table: "Packages",
type: "decimal(18,4)",
precision: 18,
scale: 4,
nullable: false,
defaultValue: 2.5m);
migrationBuilder.AddColumn<int>(
name: "MaxBalancesPerLeg",
schema: "CMS",
table: "Packages",
type: "int",
nullable: false,
defaultValue: 300);
migrationBuilder.AddColumn<int>(
name: "MaxNetworkLevel",
schema: "CMS",
table: "Packages",
type: "int",
nullable: false,
defaultValue: 15);
migrationBuilder.AddColumn<int>(
name: "SortOrder",
schema: "CMS",
table: "Packages",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<bool>(
name: "SupportsDayaPurchase",
schema: "CMS",
table: "Packages",
type: "bit",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "SupportsDirectPurchase",
schema: "CMS",
table: "Packages",
type: "bit",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<long>(
name: "PackageId",
schema: "CMS",
table: "NetworkWeeklyBalances",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.AddColumn<DateTime>(
name: "FirstActivationDate",
schema: "CMS",
table: "ClubMemberships",
type: "datetime2",
nullable: true);
migrationBuilder.AddColumn<long>(
name: "FirstPackageId",
schema: "CMS",
table: "ClubMemberships",
type: "bigint",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "LastActivationDate",
schema: "CMS",
table: "ClubMemberships",
type: "datetime2",
nullable: true);
migrationBuilder.AddColumn<long>(
name: "LastPackageId",
schema: "CMS",
table: "ClubMemberships",
type: "bigint",
nullable: true);
migrationBuilder.AddColumn<long>(
name: "PackageId",
schema: "CMS",
table: "ClubMembershipCycles",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.CreateTable(
name: "PackageFeatures",
schema: "CMS",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
PackageId = table.Column<long>(type: "bigint", nullable: false),
ClubFeatureId = table.Column<long>(type: "bigint", nullable: false),
IsIncluded = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PackageFeatures", x => x.Id);
table.ForeignKey(
name: "FK_PackageFeatures_ClubFeatures_ClubFeatureId",
column: x => x.ClubFeatureId,
principalSchema: "CMS",
principalTable: "ClubFeatures",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_PackageFeatures_Packages_PackageId",
column: x => x.PackageId,
principalSchema: "CMS",
principalTable: "Packages",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_WeeklyCommissionPool_WeekDef_Package",
schema: "CMS",
table: "WeeklyCommissionPools",
columns: new[] { "WeekDefinitionId", "PackageId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_WeeklyCommissionPools_PackageId",
schema: "CMS",
table: "WeeklyCommissionPools",
column: "PackageId");
migrationBuilder.CreateIndex(
name: "IX_UserWalletChangeLogs_PackageId",
schema: "CMS",
table: "UserWalletChangeLogs",
column: "PackageId");
migrationBuilder.CreateIndex(
name: "IX_UserCommissionPayout_User_WeekDef_Package",
schema: "CMS",
table: "UserCommissionPayouts",
columns: new[] { "UserId", "WeekDefinitionId", "PackageId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_UserCommissionPayouts_PackageId",
schema: "CMS",
table: "UserCommissionPayouts",
column: "PackageId");
migrationBuilder.CreateIndex(
name: "IX_Package_IsActive",
schema: "CMS",
table: "Packages",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_Package_SortOrder",
schema: "CMS",
table: "Packages",
column: "SortOrder");
migrationBuilder.CreateIndex(
name: "IX_NetworkWeeklyBalance_User_WeekDef_Package",
schema: "CMS",
table: "NetworkWeeklyBalances",
columns: new[] { "UserId", "WeekDefinitionId", "PackageId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_NetworkWeeklyBalances_PackageId",
schema: "CMS",
table: "NetworkWeeklyBalances",
column: "PackageId");
migrationBuilder.CreateIndex(
name: "IX_ClubMembership_LastActivationDate",
schema: "CMS",
table: "ClubMemberships",
column: "LastActivationDate");
migrationBuilder.CreateIndex(
name: "IX_ClubMemberships_FirstPackageId",
schema: "CMS",
table: "ClubMemberships",
column: "FirstPackageId");
migrationBuilder.CreateIndex(
name: "IX_ClubMemberships_LastPackageId",
schema: "CMS",
table: "ClubMemberships",
column: "LastPackageId");
migrationBuilder.CreateIndex(
name: "IX_ClubMembershipCycles_PackageId",
schema: "CMS",
table: "ClubMembershipCycles",
column: "PackageId");
migrationBuilder.CreateIndex(
name: "IX_PackageFeature_PackageId_ClubFeatureId",
schema: "CMS",
table: "PackageFeatures",
columns: new[] { "PackageId", "ClubFeatureId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PackageFeatures_ClubFeatureId",
schema: "CMS",
table: "PackageFeatures",
column: "ClubFeatureId");
// === DATA MIGRATION: Seed golden package and backfill existing records ===
// 1. Ensure golden package exists with known Id
// Use IDENTITY_INSERT to guarantee Id=1 (if Package table uses identity)
migrationBuilder.Sql(@"
SET IDENTITY_INSERT [CMS].[Packages] ON;
IF NOT EXISTS (SELECT 1 FROM [CMS].[Packages] WHERE [Id] = 1)
BEGIN
INSERT INTO [CMS].[Packages]
([Id], [Title], [Description], [ImagePath], [Price],
[SortOrder], [IsActive], [IsBasePackage],
[SupportsDayaPurchase], [SupportsDirectPurchase],
[ActivationFee], [DiscountMultiplier], [MagicWalletMultiplier],
[MaxBalancesPerLeg], [MaxNetworkLevel],
[MagicWalletMaxDeposit], [MagicWalletMaxCredit],
[Created], [IsDeleted])
VALUES
(1, N'پکیج طلایی', N'پکیج اصلی باشگاه مشتریان کارا بازار سلامت',
N'/images/packages/golden.png', 56000000,
1, 1, 1,
1, 1,
25200000, 2.0, 2.5,
300, 15,
1000000000, 2500000000,
GETUTCDATE(), 0);
END
SET IDENTITY_INSERT [CMS].[Packages] OFF;
");
// 2. Backfill PackageId on existing records → point to golden package (Id=1)
migrationBuilder.Sql(@"
UPDATE [CMS].[WeeklyCommissionPools] SET [PackageId] = 1 WHERE [PackageId] = 0;
UPDATE [CMS].[UserCommissionPayouts] SET [PackageId] = 1 WHERE [PackageId] = 0;
UPDATE [CMS].[NetworkWeeklyBalances] SET [PackageId] = 1 WHERE [PackageId] = 0;
UPDATE [CMS].[ClubMembershipCycles] SET [PackageId] = 1 WHERE [PackageId] = 0;
");
// 3. Backfill ClubMembership First/Last fields from existing ActivatedAt
migrationBuilder.Sql(@"
UPDATE cm SET
cm.[FirstActivationDate] = cm.[ActivatedAt],
cm.[LastActivationDate] = cm.[ActivatedAt],
cm.[FirstPackageId] = 1,
cm.[LastPackageId] = 1
FROM [CMS].[ClubMemberships] cm
WHERE cm.[ActivatedAt] IS NOT NULL
AND cm.[FirstActivationDate] IS NULL;
");
// === END DATA MIGRATION ===
migrationBuilder.AddForeignKey(
name: "FK_ClubMembershipCycles_Packages_PackageId",
schema: "CMS",
table: "ClubMembershipCycles",
column: "PackageId",
principalSchema: "CMS",
principalTable: "Packages",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_ClubMemberships_Packages_FirstPackageId",
schema: "CMS",
table: "ClubMemberships",
column: "FirstPackageId",
principalSchema: "CMS",
principalTable: "Packages",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_ClubMemberships_Packages_LastPackageId",
schema: "CMS",
table: "ClubMemberships",
column: "LastPackageId",
principalSchema: "CMS",
principalTable: "Packages",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_NetworkWeeklyBalances_Packages_PackageId",
schema: "CMS",
table: "NetworkWeeklyBalances",
column: "PackageId",
principalSchema: "CMS",
principalTable: "Packages",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_UserCommissionPayouts_Packages_PackageId",
schema: "CMS",
table: "UserCommissionPayouts",
column: "PackageId",
principalSchema: "CMS",
principalTable: "Packages",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_UserWalletChangeLogs_Packages_PackageId",
schema: "CMS",
table: "UserWalletChangeLogs",
column: "PackageId",
principalSchema: "CMS",
principalTable: "Packages",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_WeeklyCommissionPools_Packages_PackageId",
schema: "CMS",
table: "WeeklyCommissionPools",
column: "PackageId",
principalSchema: "CMS",
principalTable: "Packages",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ClubMembershipCycles_Packages_PackageId",
schema: "CMS",
table: "ClubMembershipCycles");
migrationBuilder.DropForeignKey(
name: "FK_ClubMemberships_Packages_FirstPackageId",
schema: "CMS",
table: "ClubMemberships");
migrationBuilder.DropForeignKey(
name: "FK_ClubMemberships_Packages_LastPackageId",
schema: "CMS",
table: "ClubMemberships");
migrationBuilder.DropForeignKey(
name: "FK_NetworkWeeklyBalances_Packages_PackageId",
schema: "CMS",
table: "NetworkWeeklyBalances");
migrationBuilder.DropForeignKey(
name: "FK_UserCommissionPayouts_Packages_PackageId",
schema: "CMS",
table: "UserCommissionPayouts");
migrationBuilder.DropForeignKey(
name: "FK_UserWalletChangeLogs_Packages_PackageId",
schema: "CMS",
table: "UserWalletChangeLogs");
migrationBuilder.DropForeignKey(
name: "FK_WeeklyCommissionPools_Packages_PackageId",
schema: "CMS",
table: "WeeklyCommissionPools");
migrationBuilder.DropTable(
name: "PackageFeatures",
schema: "CMS");
migrationBuilder.DropIndex(
name: "IX_WeeklyCommissionPool_WeekDef_Package",
schema: "CMS",
table: "WeeklyCommissionPools");
migrationBuilder.DropIndex(
name: "IX_WeeklyCommissionPools_PackageId",
schema: "CMS",
table: "WeeklyCommissionPools");
migrationBuilder.DropIndex(
name: "IX_UserWalletChangeLogs_PackageId",
schema: "CMS",
table: "UserWalletChangeLogs");
migrationBuilder.DropIndex(
name: "IX_UserCommissionPayout_User_WeekDef_Package",
schema: "CMS",
table: "UserCommissionPayouts");
migrationBuilder.DropIndex(
name: "IX_UserCommissionPayouts_PackageId",
schema: "CMS",
table: "UserCommissionPayouts");
migrationBuilder.DropIndex(
name: "IX_Package_IsActive",
schema: "CMS",
table: "Packages");
migrationBuilder.DropIndex(
name: "IX_Package_SortOrder",
schema: "CMS",
table: "Packages");
migrationBuilder.DropIndex(
name: "IX_NetworkWeeklyBalance_User_WeekDef_Package",
schema: "CMS",
table: "NetworkWeeklyBalances");
migrationBuilder.DropIndex(
name: "IX_NetworkWeeklyBalances_PackageId",
schema: "CMS",
table: "NetworkWeeklyBalances");
migrationBuilder.DropIndex(
name: "IX_ClubMembership_LastActivationDate",
schema: "CMS",
table: "ClubMemberships");
migrationBuilder.DropIndex(
name: "IX_ClubMemberships_FirstPackageId",
schema: "CMS",
table: "ClubMemberships");
migrationBuilder.DropIndex(
name: "IX_ClubMemberships_LastPackageId",
schema: "CMS",
table: "ClubMemberships");
migrationBuilder.DropIndex(
name: "IX_ClubMembershipCycles_PackageId",
schema: "CMS",
table: "ClubMembershipCycles");
migrationBuilder.DropColumn(
name: "PackageId",
schema: "CMS",
table: "WeeklyCommissionPools");
migrationBuilder.DropColumn(
name: "PackageId",
schema: "CMS",
table: "UserWalletChangeLogs");
migrationBuilder.DropColumn(
name: "PackageId",
schema: "CMS",
table: "UserCommissionPayouts");
migrationBuilder.DropColumn(
name: "ActivationFee",
schema: "CMS",
table: "Packages");
migrationBuilder.DropColumn(
name: "DiscountMultiplier",
schema: "CMS",
table: "Packages");
migrationBuilder.DropColumn(
name: "IsActive",
schema: "CMS",
table: "Packages");
migrationBuilder.DropColumn(
name: "IsBasePackage",
schema: "CMS",
table: "Packages");
migrationBuilder.DropColumn(
name: "MagicWalletMaxCredit",
schema: "CMS",
table: "Packages");
migrationBuilder.DropColumn(
name: "MagicWalletMaxDeposit",
schema: "CMS",
table: "Packages");
migrationBuilder.DropColumn(
name: "MagicWalletMultiplier",
schema: "CMS",
table: "Packages");
migrationBuilder.DropColumn(
name: "MaxBalancesPerLeg",
schema: "CMS",
table: "Packages");
migrationBuilder.DropColumn(
name: "MaxNetworkLevel",
schema: "CMS",
table: "Packages");
migrationBuilder.DropColumn(
name: "SortOrder",
schema: "CMS",
table: "Packages");
migrationBuilder.DropColumn(
name: "SupportsDayaPurchase",
schema: "CMS",
table: "Packages");
migrationBuilder.DropColumn(
name: "SupportsDirectPurchase",
schema: "CMS",
table: "Packages");
migrationBuilder.DropColumn(
name: "PackageId",
schema: "CMS",
table: "NetworkWeeklyBalances");
migrationBuilder.DropColumn(
name: "FirstActivationDate",
schema: "CMS",
table: "ClubMemberships");
migrationBuilder.DropColumn(
name: "FirstPackageId",
schema: "CMS",
table: "ClubMemberships");
migrationBuilder.DropColumn(
name: "LastActivationDate",
schema: "CMS",
table: "ClubMemberships");
migrationBuilder.DropColumn(
name: "LastPackageId",
schema: "CMS",
table: "ClubMemberships");
migrationBuilder.DropColumn(
name: "PackageId",
schema: "CMS",
table: "ClubMembershipCycles");
migrationBuilder.CreateIndex(
name: "IX_WeeklyCommissionPool_WeekDefinitionId",
schema: "CMS",
table: "WeeklyCommissionPools",
column: "WeekDefinitionId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_UserCommissionPayout_UserId_WeekDefinitionId",
schema: "CMS",
table: "UserCommissionPayouts",
columns: new[] { "UserId", "WeekDefinitionId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_NetworkWeeklyBalance_UserId_WeekDefinitionId",
schema: "CMS",
table: "NetworkWeeklyBalances",
columns: new[] { "UserId", "WeekDefinitionId" },
unique: true);
}
}
}
@@ -400,6 +400,12 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("FirstActivationDate")
.HasColumnType("datetime2");
b.Property<long?>("FirstPackageId")
.HasColumnType("bigint");
b.Property<long>("GiftValue")
.HasColumnType("bigint");
@@ -412,12 +418,18 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTime?>("LastActivationDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("LastModified")
.HasColumnType("datetime2");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<long?>("LastPackageId")
.HasColumnType("bigint");
b.Property<int>("PurchaseMethod")
.HasColumnType("int");
@@ -429,9 +441,16 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.HasKey("Id");
b.HasIndex("FirstPackageId");
b.HasIndex("IsActive")
.HasDatabaseName("IX_ClubMembership_IsActive");
b.HasIndex("LastActivationDate")
.HasDatabaseName("IX_ClubMembership_LastActivationDate");
b.HasIndex("LastPackageId");
b.HasIndex("UserId")
.IsUnique()
.HasDatabaseName("IX_ClubMembership_UserId");
@@ -482,6 +501,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<long>("PackageAmount")
.HasColumnType("bigint");
b.Property<long>("PackageId")
.HasColumnType("bigint");
b.Property<DateTime>("PackagePurchasedAt")
.HasColumnType("datetime2");
@@ -495,6 +517,8 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.HasIndex("ClubMembershipId");
b.HasIndex("PackageId");
b.HasIndex("PackagePurchasedAt")
.HasDatabaseName("IX_ClubMembershipCycle_PackagePurchasedAt");
@@ -598,6 +622,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<long>("PackageId")
.HasColumnType("bigint");
b.Property<DateTime?>("PaidAt")
.HasColumnType("datetime2");
@@ -641,6 +668,8 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.HasKey("Id");
b.HasIndex("PackageId");
b.HasIndex("Status")
.HasDatabaseName("IX_UserCommissionPayout_Status");
@@ -649,9 +678,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.HasIndex("WeeklyPoolId")
.HasDatabaseName("IX_UserCommissionPayout_WeeklyPoolId");
b.HasIndex("UserId", "WeekDefinitionId")
b.HasIndex("UserId", "WeekDefinitionId", "PackageId")
.IsUnique()
.HasDatabaseName("IX_UserCommissionPayout_UserId_WeekDefinitionId");
.HasDatabaseName("IX_UserCommissionPayout_User_WeekDef_Package");
b.ToTable("UserCommissionPayouts", "CMS");
});
@@ -685,6 +714,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<long>("PackageId")
.HasColumnType("bigint");
b.Property<int>("TotalBalances")
.HasColumnType("int");
@@ -702,9 +734,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.HasIndex("IsCalculated")
.HasDatabaseName("IX_WeeklyCommissionPool_IsCalculated");
b.HasIndex("WeekDefinitionId")
b.HasIndex("PackageId");
b.HasIndex("WeekDefinitionId", "PackageId")
.IsUnique()
.HasDatabaseName("IX_WeeklyCommissionPool_WeekDefinitionId");
.HasDatabaseName("IX_WeeklyCommissionPool_WeekDef_Package");
b.ToTable("WeeklyCommissionPools", "CMS");
});
@@ -2242,6 +2276,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<int>("LeftLegTotal")
.HasColumnType("int");
b.Property<long>("PackageId")
.HasColumnType("bigint");
b.Property<int>("RightLegBalances")
.HasColumnType("int");
@@ -2280,12 +2317,14 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.HasIndex("IsExpired")
.HasDatabaseName("IX_NetworkWeeklyBalance_IsExpired");
b.HasIndex("PackageId");
b.HasIndex("WeekDefinitionId")
.HasDatabaseName("IX_NetworkWeeklyBalance_WeekDefinitionId");
b.HasIndex("UserId", "WeekDefinitionId")
b.HasIndex("UserId", "WeekDefinitionId", "PackageId")
.IsUnique()
.HasDatabaseName("IX_NetworkWeeklyBalance_UserId_WeekDefinitionId");
.HasDatabaseName("IX_NetworkWeeklyBalance_User_WeekDef_Package");
b.ToTable("NetworkWeeklyBalances", "CMS");
});
@@ -2412,6 +2451,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<long>("ActivationFee")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasDefaultValue(0L);
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
@@ -2422,10 +2466,26 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<decimal>("DiscountMultiplier")
.ValueGeneratedOnAdd()
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)")
.HasDefaultValue(2.0m);
b.Property<string>("ImagePath")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.Property<bool>("IsBasePackage")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
@@ -2435,18 +2495,110 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<long>("MagicWalletMaxCredit")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasDefaultValue(2500000000L);
b.Property<long>("MagicWalletMaxDeposit")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasDefaultValue(1000000000L);
b.Property<decimal>("MagicWalletMultiplier")
.ValueGeneratedOnAdd()
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)")
.HasDefaultValue(2.5m);
b.Property<int>("MaxBalancesPerLeg")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(300);
b.Property<int>("MaxNetworkLevel")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(15);
b.Property<long>("Price")
.HasColumnType("bigint");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<bool>("SupportsDayaPurchase")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("SupportsDirectPurchase")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.Property<string>("Title")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("IsActive")
.HasDatabaseName("IX_Package_IsActive");
b.HasIndex("SortOrder")
.HasDatabaseName("IX_Package_SortOrder");
b.ToTable("Packages", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.PackageFeature", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<long>("ClubFeatureId")
.HasColumnType("bigint");
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<bool>("IsIncluded")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.Property<DateTime?>("LastModified")
.HasColumnType("datetime2");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<long>("PackageId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("ClubFeatureId");
b.HasIndex("PackageId", "ClubFeatureId")
.IsUnique()
.HasDatabaseName("IX_PackageFeature_PackageId_ClubFeatureId");
b.ToTable("PackageFeatures", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Payment.ManualPayment", b =>
{
b.Property<long>("Id")
@@ -3718,6 +3870,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<long?>("PackageId")
.HasColumnType("bigint");
b.Property<long?>("RefrenceId")
.HasColumnType("bigint");
@@ -3726,6 +3881,8 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.HasKey("Id");
b.HasIndex("PackageId");
b.HasIndex("WalletId");
b.ToTable("UserWalletChangeLogs", "CMS");
@@ -3947,12 +4104,26 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Club.ClubMembership", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.Package", "FirstPackage")
.WithMany()
.HasForeignKey("FirstPackageId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("CMSMicroservice.Domain.Entities.Package", "LastPackage")
.WithMany()
.HasForeignKey("LastPackageId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("CMSMicroservice.Domain.Entities.User", "User")
.WithOne("ClubMembership")
.HasForeignKey("CMSMicroservice.Domain.Entities.Club.ClubMembership", "UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("FirstPackage");
b.Navigation("LastPackage");
b.Navigation("User");
});
@@ -3964,6 +4135,12 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
.WithMany()
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CMSMicroservice.Domain.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
@@ -3972,6 +4149,8 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Navigation("ClubMembership");
b.Navigation("Package");
b.Navigation("User");
});
@@ -4004,6 +4183,12 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Commission.UserCommissionPayout", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
.WithMany()
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CMSMicroservice.Domain.Entities.User", "User")
.WithMany("CommissionPayouts")
.HasForeignKey("UserId")
@@ -4022,6 +4207,8 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Package");
b.Navigation("User");
b.Navigation("WeekDefinition");
@@ -4031,12 +4218,20 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Commission.WeeklyCommissionPool", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
.WithMany()
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CMSMicroservice.Domain.Entities.WeekDefinition", "WeekDefinition")
.WithMany("WeeklyCommissionPools")
.HasForeignKey("WeekDefinitionId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Package");
b.Navigation("WeekDefinition");
});
@@ -4292,6 +4487,12 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Network.NetworkWeeklyBalance", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
.WithMany()
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CMSMicroservice.Domain.Entities.User", "User")
.WithMany("NetworkWeeklyBalances")
.HasForeignKey("UserId")
@@ -4304,6 +4505,8 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Package");
b.Navigation("User");
b.Navigation("WeekDefinition");
@@ -4320,6 +4523,25 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Navigation("Order");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.PackageFeature", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.Club.ClubFeature", "ClubFeature")
.WithMany()
.HasForeignKey("ClubFeatureId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
.WithMany("PackageFeatures")
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ClubFeature");
b.Navigation("Package");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Payment.ManualPayment", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.Transaction", "Transaction")
@@ -4504,7 +4726,7 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
.WithMany()
.WithMany("Purchases")
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
@@ -4561,12 +4783,19 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserWalletChangeLog", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
.WithMany()
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("CMSMicroservice.Domain.Entities.UserWallet", "Wallet")
.WithMany("UserWalletChangeLogs")
.HasForeignKey("WalletId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Package");
b.Navigation("Wallet");
});
@@ -4670,6 +4899,10 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Package", b =>
{
b.Navigation("PackageFeatures");
b.Navigation("Purchases");
b.Navigation("UserOrders");
});
@@ -8,6 +8,9 @@
CREATE OR ALTER PROCEDURE [CMS].[sp_CalculateWeeklyBalances]
@WeekDefinitionId BIGINT,
@ForceRecalculate BIT = 0,
@PackageId BIGINT = NULL, -- پکیج خاص (اگر NULL از پکیج پایه استفاده می‌کند)
@InputMaxBalancesPerLeg INT = NULL, -- سقف تعادل هر پا (از Package entity)
@InputMaxNetworkLevel INT = NULL, -- حداکثر عمق شبکه (از Package entity)
@RowCount INT OUTPUT
AS
BEGIN
@@ -18,10 +21,33 @@ BEGIN
DECLARE @StartDate DATETIME;
DECLARE @EndDate DATETIME;
DECLARE @PreviousWeekDefinitionId BIGINT;
DECLARE @MaxBalancesPerLeg INT = 300;
DECLARE @MaxNetworkLevel INT = 15;
DECLARE @MaxBalancesPerLeg INT;
DECLARE @MaxNetworkLevel INT;
DECLARE @CalculatedAt DATETIME = GETDATE();
-- تعیین PackageId پیش‌فرض (پکیج پایه)
IF @PackageId IS NULL
BEGIN
SELECT TOP 1 @PackageId = Id
FROM CMS.Packages
WHERE IsBasePackage = 1 AND IsDeleted = 0;
END
-- خواندن تنظیمات از Package entity اگر پارامتر داده نشده
IF @InputMaxBalancesPerLeg IS NOT NULL
SET @MaxBalancesPerLeg = @InputMaxBalancesPerLeg;
ELSE
SELECT @MaxBalancesPerLeg = ISNULL(MaxBalancesPerLeg, 300) FROM CMS.Packages WHERE Id = @PackageId;
IF @InputMaxNetworkLevel IS NOT NULL
SET @MaxNetworkLevel = @InputMaxNetworkLevel;
ELSE
SELECT @MaxNetworkLevel = ISNULL(MaxNetworkLevel, 15) FROM CMS.Packages WHERE Id = @PackageId;
-- fallbackهای امن
SET @MaxBalancesPerLeg = ISNULL(@MaxBalancesPerLeg, 300);
SET @MaxNetworkLevel = ISNULL(@MaxNetworkLevel, 15);
BEGIN TRY
BEGIN TRANSACTION;
@@ -65,11 +91,9 @@ BEGIN
AND IsActive = 1;
-- =============================================
-- 4. مقادیر ثابت (Hardcoded - از SystemConstants)
-- 4. تنظیمات پکیج (از پارامترها خوانده می‌شود)
-- =============================================
-- این مقادیر ثابت هستند و تغییر نمی‌کنند
SET @MaxBalancesPerLeg = 300; -- سقف تعادل هر پا
SET @MaxNetworkLevel = 15; -- حداکثر عمق شبکه
-- @MaxBalancesPerLeg و @MaxNetworkLevel قبلاً مقداردهی شده‌اند
-- =============================================
-- 5. ایجاد جدول موقت برای نتایج
@@ -96,7 +120,8 @@ BEGIN
INSERT INTO #Balances (UserId)
SELECT DISTINCT u.Id
FROM CMS.Users u
INNER JOIN CMS.ClubMemberships cm ON cm.UserId = u.Id AND cm.IsActive = 1;
INNER JOIN CMS.ClubMemberships cm ON cm.UserId = u.Id AND cm.IsActive = 1
WHERE cm.LastPackageId = @PackageId;
-- =============================================
-- 7. دریافت باقیمانده هفته قبل
@@ -107,7 +132,9 @@ BEGIN
SET b.LeftLegCarryover = ISNULL(nb.LeftLegRemainder, 0),
b.RightLegCarryover = ISNULL(nb.RightLegRemainder, 0)
FROM #Balances b
LEFT JOIN CMS.NetworkWeeklyBalances nb ON nb.UserId = b.UserId AND nb.WeekDefinitionId = @PreviousWeekDefinitionId;
LEFT JOIN CMS.NetworkWeeklyBalances nb ON nb.UserId = b.UserId
AND nb.WeekDefinitionId = @PreviousWeekDefinitionId
AND nb.PackageId = @PackageId;
END
-- =============================================
@@ -269,6 +296,7 @@ BEGIN
INSERT INTO CMS.NetworkWeeklyBalances (
UserId,
WeekDefinitionId,
PackageId,
LeftLegNewMembers,
RightLegNewMembers,
LeftLegCarryover,
@@ -295,6 +323,7 @@ BEGIN
SELECT
UserId,
@WeekDefinitionId,
@PackageId,
LeftLegNewMembers,
RightLegNewMembers,
LeftLegCarryover,
@@ -62,10 +62,20 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy
.Select(w => w.UserId)
.ToHashSetAsync(cancellationToken);
var activeClubMemberUserIds = await _context.ClubMemberships
// دریافت عضویت‌های فعال به همراه PackageId هر کاربر
var activeClubMemberships = await _context.ClubMemberships
.Where(c => c.IsActive && !magicModeUserIds.Contains(c.UserId))
.Select(c => new { c.UserId, PackageId = c.LastPackageId })
.ToListAsync(cancellationToken);
var activeClubMemberUserIds = activeClubMemberships
.Select(c => c.UserId)
.ToHashSetAsync(cancellationToken);
.ToHashSet();
// نگاشت کاربر → PackageId
var userPackageMap = activeClubMemberships
.Where(c => c.PackageId.HasValue)
.ToDictionary(c => c.UserId, c => c.PackageId!.Value);
// دریافت کاربران فعال در شبکه که عضو باشگاه هستند
var usersInNetwork = await _context.Users
@@ -73,40 +83,52 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy
.Select(x => new { x.Id })
.ToListAsync(cancellationToken);
// دریافت باقیمانده‌های هفته قبل
// بارگذاری همه پکیج‌های فعال (bulk load)
var allPackages = await _context.Packages
.Where(p => !p.IsDeleted)
.ToDictionaryAsync(p => p.Id, cancellationToken);
// پکیج پیش‌فرض (fallback برای کاربرانی که PackageId ندارند)
var defaultPackage = allPackages.Values
.FirstOrDefault(p => p.IsBasePackage)
?? throw new InvalidOperationException("پکیج پایه یافت نشد");
// دریافت باقیمانده‌های هفته قبل — شامل PackageId
var previousWeekDefinitionId = GetPreviousWeekDefinitionId(weekDefinitionId);
Dictionary<long, (int LeftLegRemainder, int RightLegRemainder)> previousWeekCarryovers;
Dictionary<(long UserId, long PackageId), (int LeftLegRemainder, int RightLegRemainder)> previousWeekCarryovers;
if (previousWeekDefinitionId.HasValue)
{
previousWeekCarryovers = await _context.NetworkWeeklyBalances
.Where(x => x.WeekDefinitionId == previousWeekDefinitionId.Value)
.ToDictionaryAsync(
x => x.UserId,
x => (x.UserId, x.PackageId),
x => (x.LeftLegRemainder, x.RightLegRemainder),
cancellationToken);
}
else
{
previousWeekCarryovers = new Dictionary<long, (int, int)>();
previousWeekCarryovers = new Dictionary<(long, long), (int, int)>();
}
var balancesList = new List<NetworkWeeklyBalance>();
var calculatedAt = DateTime.Now;
// استفاده از SystemConstants به جای دیتابیس
var maxBalancesPerLeg = SystemConstants.CommissionMaxWeeklyBalancesPerLeg;
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
foreach (var user in usersInNetwork.OrderBy(o => o.Id))
{
// دریافت باقیمانده هفته قبل
// تعیین پکیج هر کاربر (per-user)
var userPackageId = userPackageMap.GetValueOrDefault(user.Id, defaultPackage.Id);
var package = allPackages.GetValueOrDefault(userPackageId, defaultPackage);
var maxBalancesPerLeg = package.MaxBalancesPerLeg;
var maxNetworkLevel = package.MaxNetworkLevel;
// دریافت باقیمانده هفته قبل — فیلتر per-package
var leftCarryover = 0;
var rightCarryover = 0;
if (previousWeekCarryovers.ContainsKey(user.Id))
var carryoverKey = (UserId: user.Id, PackageId: userPackageId);
if (previousWeekCarryovers.ContainsKey(carryoverKey))
{
leftCarryover = previousWeekCarryovers[user.Id].LeftLegRemainder;
rightCarryover = previousWeekCarryovers[user.Id].RightLegRemainder;
leftCarryover = previousWeekCarryovers[carryoverKey].LeftLegRemainder;
rightCarryover = previousWeekCarryovers[carryoverKey].RightLegRemainder;
}
// محاسبه تعداد اعضای جدید در این هفته (تا maxNetworkLevel لول پایین‌تر)
@@ -135,6 +157,7 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy
{
UserId = user.Id,
WeekDefinitionId = weekDefinitionId,
PackageId = package.Id,
LeftLegNewMembers = leftNewMembers,
RightLegNewMembers = rightNewMembers,
LeftLegCarryover = leftCarryover,
@@ -162,15 +185,16 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy
await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
// محاسبه تعادل زیرمجموعه
// محاسبه تعادل زیرمجموعه — per-user maxNetworkLevel
var balancesDictionary = balancesList.ToDictionary(x => x.UserId);
foreach (var balance in balancesList)
{
var balancePackage = allPackages.GetValueOrDefault(balance.PackageId, defaultPackage);
var subordinateBalances = await CalculateSubordinateBalancesAsync(
balance.UserId,
balancesDictionary,
maxNetworkLevel,
balancePackage.MaxNetworkLevel,
cancellationToken
);
@@ -284,6 +308,7 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy
UserId = balance.UserId,
WeekDefinitionId = weekDefinitionId,
WeeklyPoolId = existingPool.Id,
PackageId = balance.PackageId, // per-user PackageId از محاسبه تعادل
BalancesEarned = userBalance,
ValuePerBalance = valuePerBalance,
TotalAmount = totalAmount,
@@ -6,6 +6,7 @@ namespace CMSMicroservice.Infrastructure.Services.Commission;
/// <summary>
/// پیاده‌سازی محاسبه کمیسیون با استفاده از Stored Procedure
/// این روش برای داده‌های زیاد بهینه‌تر است و از CTE استفاده می‌کند
/// حلقه روی پکیج‌های فعال — هر پکیج با تنظیمات خودش
/// </summary>
public class StoredProcedureCommissionCalculationStrategy : ICommissionCalculationStrategy
{
@@ -22,45 +23,81 @@ public class StoredProcedureCommissionCalculationStrategy : ICommissionCalculati
bool forceRecalculate,
CancellationToken cancellationToken = default)
{
// دسترسی به DbContext واقعی برای اجرای SP
var dbContext = _context as DbContext;
if (dbContext == null)
{
throw new InvalidOperationException("DbContext برای اجرای Stored Procedure در دسترس نیست");
}
// بارگذاری پکیج‌های فعال
var activePackages = await _context.Packages
.Where(p => !p.IsDeleted && p.IsActive)
.ToListAsync(cancellationToken);
if (!activePackages.Any())
{
throw new InvalidOperationException("هیچ پکیج فعالی یافت نشد");
}
var totalRowCount = 0;
var connection = dbContext.Database.GetDbConnection();
await connection.OpenAsync(cancellationToken);
try
{
using var command = connection.CreateCommand();
command.CommandText = "CMS.sp_CalculateWeeklyBalances";
command.CommandType = System.Data.CommandType.StoredProcedure;
command.CommandTimeout = 300; // 5 دقیقه timeout
// حلقه روی پکیج‌ها — هر پکیج با تنظیمات خودش
foreach (var package in activePackages)
{
using var command = connection.CreateCommand();
command.CommandText = "CMS.sp_CalculateWeeklyBalances";
command.CommandType = System.Data.CommandType.StoredProcedure;
command.CommandTimeout = 300; // 5 دقیقه timeout
// پارامترها
var paramWeekDefinitionId = command.CreateParameter();
paramWeekDefinitionId.ParameterName = "@WeekDefinitionId";
paramWeekDefinitionId.Value = weekDefinitionId;
command.Parameters.Add(paramWeekDefinitionId);
// پارامتر WeekDefinitionId
var paramWeekDefinitionId = command.CreateParameter();
paramWeekDefinitionId.ParameterName = "@WeekDefinitionId";
paramWeekDefinitionId.Value = weekDefinitionId;
command.Parameters.Add(paramWeekDefinitionId);
var paramForceRecalculate = command.CreateParameter();
paramForceRecalculate.ParameterName = "@ForceRecalculate";
paramForceRecalculate.Value = forceRecalculate;
command.Parameters.Add(paramForceRecalculate);
// پارامتر ForceRecalculate
var paramForceRecalculate = command.CreateParameter();
paramForceRecalculate.ParameterName = "@ForceRecalculate";
paramForceRecalculate.Value = forceRecalculate;
command.Parameters.Add(paramForceRecalculate);
// پارامتر خروجی
var paramRowCount = command.CreateParameter();
paramRowCount.ParameterName = "@RowCount";
paramRowCount.Direction = System.Data.ParameterDirection.Output;
paramRowCount.DbType = System.Data.DbType.Int32;
command.Parameters.Add(paramRowCount);
// پارامتر PackageId — per-package
var paramPackageId = command.CreateParameter();
paramPackageId.ParameterName = "@PackageId";
paramPackageId.Value = package.Id;
command.Parameters.Add(paramPackageId);
await command.ExecuteNonQueryAsync(cancellationToken);
// پارامتر MaxBalancesPerLeg — per-package
var paramMaxBalances = command.CreateParameter();
paramMaxBalances.ParameterName = "@InputMaxBalancesPerLeg";
paramMaxBalances.Value = package.MaxBalancesPerLeg;
command.Parameters.Add(paramMaxBalances);
var rowCount = (int)(paramRowCount.Value ?? 0);
return rowCount;
// پارامتر MaxNetworkLevel — per-package
var paramMaxLevel = command.CreateParameter();
paramMaxLevel.ParameterName = "@InputMaxNetworkLevel";
paramMaxLevel.Value = package.MaxNetworkLevel;
command.Parameters.Add(paramMaxLevel);
// پارامتر خروجی
var paramRowCount = command.CreateParameter();
paramRowCount.ParameterName = "@RowCount";
paramRowCount.Direction = System.Data.ParameterDirection.Output;
paramRowCount.DbType = System.Data.DbType.Int32;
command.Parameters.Add(paramRowCount);
await command.ExecuteNonQueryAsync(cancellationToken);
var rowCount = (int)(paramRowCount.Value ?? 0);
totalRowCount += rowCount;
}
return totalRowCount;
}
finally
{
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.184</Version>
<Version>0.0.188</Version>
<DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
@@ -64,8 +64,8 @@
<Protobuf Include="Protos\appversion.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<!-- Inventory Management System -->
<Protobuf Include="Protos\inventory.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<!-- FMS (File Management Service) - gRPC Client only -->
<Protobuf Include="Protos\fms.proto" ProtoRoot="Protos\" GrpcServices="Client" />
<!-- [ARCHIVED] FMS (File Management Service) - سرویس هرگز پیاده‌سازی نشد. فایل‌ها از ImageResolver استفاده می‌کنند -->
<!-- <Protobuf Include="Protos\fms.proto" ProtoRoot="Protos\" GrpcServices="Client" /> -->
<!-- Blog & Content Management System -->
<Protobuf Include="Protos\blogpost.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<Protobuf Include="Protos\blogcategory.proto" ProtoRoot="Protos\" GrpcServices="Both" />
@@ -217,6 +217,7 @@ message GetUserCommissionPayoutsRequest
google.protobuf.Int64Value user_id = 1;
google.protobuf.Int32Value status = 2; // CommissionPayoutStatus enum
google.protobuf.Int64Value week_definition_id = 3; // Preferred filter
google.protobuf.Int64Value package_id = 4; // فیلتر بر اساس پکیج
int32 page_index = 5;
int32 page_size = 6;
}
@@ -244,6 +245,8 @@ message UserCommissionPayoutModel
string iban_number = 13;
google.protobuf.Timestamp withdrawn_at = 14;
google.protobuf.Timestamp created = 15;
int64 package_id = 16; // شناسه پکیج
string package_title = 17; // نام پکیج
}
// GetCommissionPayoutHistory Query
@@ -314,6 +317,7 @@ message GetUserWeeklyBalancesRequest
{
google.protobuf.Int64Value user_id = 1;
google.protobuf.Int64Value week_definition_id = 2; // Preferred filter
google.protobuf.Int64Value package_id = 3; // فیلتر بر اساس پکیج
bool only_active = 4; // Only non-expired balances
int32 page_index = 5;
int32 page_size = 6;
@@ -343,6 +347,8 @@ message UserWeeklyBalanceModel
google.protobuf.Timestamp calculated_at = 14;
bool is_expired = 15;
google.protobuf.Timestamp created = 16;
int64 package_id = 17; // شناسه پکیج
string package_title = 18; // نام پکیج
}
// GetAllWeeklyPools Query
@@ -598,6 +604,7 @@ message GetMyCommissionPayoutsRequest
int32 page_size = 2;
google.protobuf.Int64Value week_definition_id = 3;
google.protobuf.Int32Value status = 4; // 0=Pending, 1=Calculated, 2=Paid, 3=Withdrawn
google.protobuf.Int64Value package_id = 5; // فیلتر بر اساس پکیج
}
message GetMyCommissionPayoutsResponse
@@ -622,6 +629,8 @@ message CustomerCommissionPayoutModel
int32 status = 7; // 0=Pending, 1=Paid, 2=WithdrawRequested, 3=Withdrawn, 4=PaymentFailed, 5=Cancelled
google.protobuf.Timestamp calculated_date = 8;
string date_persian = 9;
int64 package_id = 10; // شناسه پکیج
string package_title = 11; // نام پکیج
}
// GetMyWeeklyBalances - for frontend customer display
@@ -631,6 +640,7 @@ message GetMyWeeklyBalancesRequest
int32 page_size = 2;
google.protobuf.Int64Value week_definition_id = 3;
bool only_active = 4;
google.protobuf.Int64Value package_id = 5; // فیلتر بر اساس پکیج
}
message GetMyWeeklyBalancesResponse
@@ -658,4 +668,6 @@ message CustomerWeeklyBalanceModel
int32 right_leg_carryover = 12;
int32 left_leg_new_members = 13;
int32 right_leg_new_members = 14;
int64 package_id = 15; // شناسه پکیج
string package_title = 16; // نام پکیج
}
@@ -153,7 +153,7 @@ message GetUserNetworkResponse
bool has_received_daya_credit = 32;
google.protobuf.Timestamp daya_credit_received_at = 33;
int32 package_purchase_method = 34;
bool has_purchased_golden_package = 35;
bool has_purchased_package = 35;
// آمار مالی
double total_earned_commission = 36;
+63 -100
View File
@@ -44,39 +44,12 @@ service PackageContract
};
};
// Package Purchase System
rpc PurchaseGoldenPackage(PurchaseGoldenPackageRequest) returns (PurchaseGoldenPackageResponse){
option (google.api.http) = {
post: "/PurchaseGoldenPackage"
body: "*"
};
};
rpc VerifyGoldenPackagePurchase(VerifyGoldenPackagePurchaseRequest) returns (VerifyGoldenPackagePurchaseResponse){
option (google.api.http) = {
post: "/VerifyGoldenPackagePurchase"
body: "*"
};
};
rpc GetUserPackageStatus(GetUserPackageStatusRequest) returns (GetUserPackageStatusResponse){
option (google.api.http) = {
get: "/GetUserPackageStatus"
};
};
// Base Package Payment (56M)
rpc InitiateBasePackagePayment(InitiateBasePackagePaymentRequest) returns (InitiateBasePackagePaymentResponse){
option (google.api.http) = {
post: "/InitiateBasePackagePayment"
body: "*"
};
};
rpc VerifyBasePackagePayment(VerifyBasePackagePaymentRequest) returns (VerifyBasePackagePaymentResponse){
option (google.api.http) = {
post: "/VerifyBasePackagePayment"
body: "*"
};
};
// ============= Customer-specific Methods =============
rpc GetCustomerPackages(GetCustomerPackagesRequest) returns (GetCustomerPackagesResponse){
@@ -114,6 +87,19 @@ message CreateNewPackageRequest
string image_path = 3;
int64 price = 4;
BoostCardFileModel image_file = 5;
int32 sort_order = 6;
bool is_active = 7;
bool is_base_package = 8;
bool supports_daya_purchase = 9;
bool supports_direct_purchase = 10;
int64 activation_fee = 11;
double discount_multiplier = 12;
double magic_wallet_multiplier = 13;
int32 max_balances_per_leg = 14;
int32 max_network_level = 15;
int64 magic_wallet_max_deposit = 16;
int64 magic_wallet_max_credit = 17;
repeated int64 feature_ids = 18;
}
message CreateNewPackageResponse
{
@@ -127,6 +113,19 @@ message UpdatePackageRequest
string image_path = 4;
int64 price = 5;
BoostCardFileModel image_file = 6;
int32 sort_order = 7;
bool is_active = 8;
bool is_base_package = 9;
bool supports_daya_purchase = 10;
bool supports_direct_purchase = 11;
int64 activation_fee = 12;
double discount_multiplier = 13;
double magic_wallet_multiplier = 14;
int32 max_balances_per_leg = 15;
int32 max_network_level = 16;
int64 magic_wallet_max_deposit = 17;
int64 magic_wallet_max_credit = 18;
repeated int64 feature_ids = 19;
}
message DeletePackageRequest
{
@@ -143,6 +142,19 @@ message GetPackageResponse
string description = 3;
string image_path = 4;
int64 price = 5;
int32 sort_order = 6;
bool is_active = 7;
bool is_base_package = 8;
bool supports_daya_purchase = 9;
bool supports_direct_purchase = 10;
int64 activation_fee = 11;
double discount_multiplier = 12;
double magic_wallet_multiplier = 13;
int32 max_balances_per_leg = 14;
int32 max_network_level = 15;
int64 magic_wallet_max_deposit = 16;
int64 magic_wallet_max_credit = 17;
repeated int64 feature_ids = 18;
}
message GetAllPackageByFilterRequest
{
@@ -170,40 +182,19 @@ message GetAllPackageByFilterResponseModel
string description = 3;
string image_path = 4;
int64 price = 5;
}
// Package Purchase Messages
message PurchaseGoldenPackageRequest
{
int64 user_id = 1;
int64 package_id = 2;
string return_url = 3;
}
message PurchaseGoldenPackageResponse
{
bool success = 1;
string message = 2;
int64 order_id = 3;
string payment_gateway_url = 4;
string tracking_code = 5;
}
message VerifyGoldenPackagePurchaseRequest
{
int64 order_id = 1;
string authority = 2;
string status = 3;
}
message VerifyGoldenPackagePurchaseResponse
{
bool success = 1;
string message = 2;
int64 order_id = 3;
int64 transaction_id = 4;
string reference_code = 5;
int64 wallet_balance = 6;
int32 sort_order = 6;
bool is_active = 7;
bool is_base_package = 8;
bool supports_daya_purchase = 9;
bool supports_direct_purchase = 10;
int64 activation_fee = 11;
double discount_multiplier = 12;
double magic_wallet_multiplier = 13;
int32 max_balances_per_leg = 14;
int32 max_network_level = 15;
int64 magic_wallet_max_deposit = 16;
int64 magic_wallet_max_credit = 17;
repeated int64 feature_ids = 18;
}
message GetUserPackageStatusRequest
@@ -224,43 +215,6 @@ message GetUserPackageStatusResponse
google.protobuf.Timestamp last_purchase_date = 9;
}
// Base Package Payment Messages
message InitiateBasePackagePaymentRequest
{
int64 user_id = 1;
string callback_url = 2; // Payment gateway callback URL
}
message InitiateBasePackagePaymentResponse
{
bool success = 1;
string message = 2;
int64 order_id = 3;
int64 transaction_id = 4;
int64 amount = 5;
string payment_gateway_url = 6;
}
message VerifyBasePackagePaymentRequest
{
int64 order_id = 1;
int64 transaction_id = 2;
bool payment_success = 3;
google.protobuf.StringValue ref_id = 4;
google.protobuf.StringValue message = 5;
}
message VerifyBasePackagePaymentResponse
{
bool success = 1;
string message = 2;
int64 order_id = 3;
int64 transaction_id = 4;
google.protobuf.StringValue reference_code = 5;
int64 wallet_balance = 6;
int64 discount_balance = 7;
}
// ============= Customer Message Types =============
message GetCustomerPackagesRequest
@@ -350,8 +304,17 @@ message CustomerPackageModel
int32 validity_days = 9;
bool is_popular = 10;
string short_description = 11;
string title = 12; // Alias for frontend compatibility (uses name)
string image_path = 13; // Alias for frontend compatibility (uses image_url)
string title = 12;
string image_path = 13;
// New package-based fields
int64 activation_fee = 14;
double discount_multiplier = 15;
double magic_wallet_multiplier = 16;
int64 magic_wallet_max_deposit = 17;
int64 magic_wallet_max_credit = 18;
bool is_base_package = 19;
bool supports_daya_purchase = 20;
bool supports_direct_purchase = 21;
}
message PackageFeature
@@ -68,7 +68,9 @@ public class CommissionProfile : IRegister
.Map(dest => dest.WithdrawnAt, src => src.WithdrawnAt.HasValue
? Timestamp.FromDateTime(src.WithdrawnAt.Value.ToUniversalTime())
: null)
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created));
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created))
.Map(dest => dest.PackageId, src => src.PackageId)
.Map(dest => dest.PackageTitle, src => src.PackageTitle);
// GetWeekDefinitions Request Mapping
config.NewConfig<GetWeekDefinitionsRequest, GetWeekDefinitionsQuery>()
@@ -127,7 +129,9 @@ public class CommissionProfile : IRegister
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.CalculatedAt.Value, DateTimeKind.Utc))
: null)
.Map(dest => dest.IsExpired, src => src.IsExpired)
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created));
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created))
.Map(dest => dest.PackageId, src => src.PackageId)
.Map(dest => dest.PackageTitle, src => src.PackageTitle);
// GetMyCommissionPayouts Request Mapping
config.NewConfig<GetMyCommissionPayoutsRequest, GetMyCommissionPayoutsQuery>()
@@ -135,6 +139,7 @@ public class CommissionProfile : IRegister
? (CommissionPayoutStatus?)src.Status.Value
: null)
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId != null ? src.WeekDefinitionId.Value : (long?)null)
.Map(dest => dest.PackageId, src => src.PackageId != null ? src.PackageId.Value : (long?)null)
.Map(dest => dest.PaginationState, src => new PaginationState
{
PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
@@ -160,12 +165,15 @@ public class CommissionProfile : IRegister
.Map(dest => dest.CalculatedDate, src => src.CalculatedDate.HasValue
? Timestamp.FromDateTime(src.CalculatedDate.Value.ToUniversalTime())
: null)
.Map(dest => dest.DatePersian, src => src.DatePersian);
.Map(dest => dest.DatePersian, src => src.DatePersian)
.Map(dest => dest.PackageId, src => src.PackageId)
.Map(dest => dest.PackageTitle, src => src.PackageTitle);
// GetMyWeeklyBalances Request Mapping
config.NewConfig<GetMyWeeklyBalancesRequest, GetMyWeeklyBalancesQuery>()
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId != null ? src.WeekDefinitionId.Value : (long?)null)
.Map(dest => dest.OnlyActive, src => src.OnlyActive)
.Map(dest => dest.PackageId, src => src.PackageId != null ? src.PackageId.Value : (long?)null)
.Map(dest => dest.PaginationState, src => new PaginationState
{
PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
@@ -199,6 +207,8 @@ public class CommissionProfile : IRegister
.Map(dest => dest.IsExpired, src => src.IsExpired)
.Map(dest => dest.CalculatedAt, src => src.CalculatedAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.CalculatedAt.Value, DateTimeKind.Utc))
: null);
: null)
.Map(dest => dest.PackageId, src => src.PackageId)
.Map(dest => dest.PackageTitle, src => src.PackageTitle);
}
}
@@ -56,7 +56,7 @@ public class NetworkMembershipProfile : IRegister
.Map(dest => dest.MaxNetworkDepth, src => src.MaxNetworkDepth)
.Map(dest => dest.HasReceivedDayaCredit, src => src.HasReceivedDayaCredit)
.Map(dest => dest.PackagePurchaseMethod, src => (int)src.PackagePurchaseMethod)
.Map(dest => dest.HasPurchasedGoldenPackage, src => src.HasPurchasedGoldenPackage)
.Map(dest => dest.HasPurchasedPackage, src => src.HasPurchasedPackage)
.Map(dest => dest.TotalEarnedCommission, src => (double)src.TotalEarnedCommission)
.Map(dest => dest.TotalPaidCommission, src => (double)src.TotalPaidCommission)
.Map(dest => dest.PendingCommission, src => (double)src.PendingCommission)
@@ -1,7 +1,3 @@
using CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
using CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
using CMSMicroservice.Application.PackageCQ.Commands.InitiateBasePackagePayment;
using CMSMicroservice.Application.PackageCQ.Commands.VerifyBasePackagePayment;
using CMSMicroservice.Application.PackageCQ.Queries.GetUserPackageStatus;
using CMSMicroservice.Protobuf.Protos.Package;
using Google.Protobuf.WellKnownTypes;
@@ -12,33 +8,6 @@ public class PackageProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// PurchaseGoldenPackage
config.NewConfig<PurchaseGoldenPackageRequest, PurchaseGoldenPackageCommand>()
.Map(dest => dest.UserId, src => src.UserId)
.Map(dest => dest.PackageId, src => src.PackageId)
.Map(dest => dest.ReturnUrl, src => src.ReturnUrl);
config.NewConfig<PurchaseGoldenPackageResponseDto, PurchaseGoldenPackageResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.PaymentGatewayUrl, src => src.PaymentGatewayUrl)
.Map(dest => dest.TrackingCode, src => src.TrackingCode);
// VerifyGoldenPackagePurchase
config.NewConfig<VerifyGoldenPackagePurchaseRequest, VerifyGoldenPackagePurchaseCommand>()
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.Authority, src => src.Authority)
.Map(dest => dest.Status, src => src.Status);
config.NewConfig<VerifyGoldenPackagePurchaseResponseDto, VerifyGoldenPackagePurchaseResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.ReferenceCode, src => src.ReferenceCode)
.Map(dest => dest.WalletBalance, src => src.WalletBalance);
// GetUserPackageStatus
config.NewConfig<GetUserPackageStatusRequest, GetUserPackageStatusQuery>()
.Map(dest => dest.UserId, src => src.UserId);
@@ -55,34 +24,6 @@ public class PackageProfile : IRegister
.Map(dest => dest.LastPurchaseDate, src => src.LastPurchaseDate.HasValue
? Timestamp.FromDateTime(src.LastPurchaseDate.Value.ToUniversalTime())
: null);
// InitiateBasePackagePayment (56M پکیج پایه)
config.NewConfig<InitiateBasePackagePaymentRequest, InitiateBasePackagePaymentCommand>()
.Map(dest => dest.UserId, src => src.UserId);
config.NewConfig<InitiateBasePackagePaymentResponseDto, InitiateBasePackagePaymentResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.Amount, src => src.Amount);
// VerifyBasePackagePayment (تأیید پرداخت پکیج پایه)
config.NewConfig<VerifyBasePackagePaymentRequest, VerifyBasePackagePaymentCommand>()
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.PaymentSuccess, src => src.PaymentSuccess)
.Map(dest => dest.RefId, src => src.RefId)
.Map(dest => dest.Message, src => src.Message);
config.NewConfig<VerifyBasePackagePaymentResponseDto, VerifyBasePackagePaymentResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.ReferenceCode, src => src.ReferenceCode)
.Map(dest => dest.WalletBalance, src => src.WalletBalance)
.Map(dest => dest.DiscountBalance, src => src.DiscountBalance);
}
}
@@ -98,8 +98,11 @@ public class CategoryService : CategoryContract.CategoryContractBase
return response;
}
#region [ARCHIVED] GetCategoryByIdForCustomer تکراری: GetCategory (admin) + GetAllCategoriesForCustomer کافی است
[Obsolete("ARCHIVED: Use GetCategory or GetAllCategoriesForCustomer instead")]
public override async Task<GetCategoryByIdForCustomerResponse> GetCategoryByIdForCustomer(GetCategoryByIdForCustomerRequest request, ServerCallContext context)
{
// [ARCHIVED] duplicate — GetCategory + GetAllCategoriesForCustomer cover this
var query = new GetCategoryQuery { Id = request.Id };
var result = await _sender.Send(query, context.CancellationToken);
@@ -118,4 +121,5 @@ public class CategoryService : CategoryContract.CategoryContractBase
}
};
}
#endregion
}
@@ -177,6 +177,8 @@ public class CityService : CityContract.CityContractBase
};
}
#region [ARCHIVED] UpdateCity & DeleteCity شهرها seed data هستند، ویرایش/حذف دستی باعث خرابی آدرسها میشود
[Obsolete("ARCHIVED: Cities are seed data — manual edit/delete breaks user addresses")]
public override async Task<Empty> UpdateCity(
UpdateCityRequest request, ServerCallContext context)
{
@@ -195,6 +197,7 @@ public class CityService : CityContract.CityContractBase
return new Empty();
}
[Obsolete("ARCHIVED: Cities are seed data — manual delete breaks user addresses")]
public override async Task<Empty> DeleteCity(
DeleteCityRequest request, ServerCallContext context)
{
@@ -206,6 +209,7 @@ public class CityService : CityContract.CityContractBase
await _context.SaveChangesAsync(context.CancellationToken);
return new Empty();
}
#endregion
#endregion
}
@@ -2,37 +2,42 @@ using CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
using CMSMicroservice.Application.Common.Authorization;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Protobuf.Protos.Configuration;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Services;
/// <summary>
/// سرویس تنظیمات سیستم - خواندن از SystemConstants
/// سرویس تنظیمات سیستم — مقادیر پکیجی از Package entity خوانده می‌شوند
/// </summary>
public class ConfigurationService : ConfigurationContract.ConfigurationContractBase
{
private readonly ILogger<ConfigurationService> _logger;
private readonly ICurrentUserService _currentUserService;
private readonly IMediator _mediator;
private readonly IApplicationDbContext _context;
public ConfigurationService(
ILogger<ConfigurationService> logger,
ICurrentUserService currentUserService,
IMediator mediator)
IMediator mediator,
IApplicationDbContext context)
{
_logger = logger;
_currentUserService = currentUserService;
_mediator = mediator;
_context = context;
}
/// <summary>
/// دریافت تنظیمات با کلید خاص
/// </summary>
public override Task<GetConfigurationByKeyResponse> GetConfigurationByKey(GetConfigurationByKeyRequest request, ServerCallContext context)
public override async Task<GetConfigurationByKeyResponse> GetConfigurationByKey(GetConfigurationByKeyRequest request, ServerCallContext context)
{
var response = new GetConfigurationByKeyResponse
{
@@ -43,30 +48,33 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
LastModified = Timestamp.FromDateTime(DateTime.UtcNow)
};
// خواندن مقدار از SystemConstants بر اساس کلید
response.Value = GetConfigurationValue(request.Key);
// برای کلیدهای پکیجی، از دیتابیس می‌خوانیم
var package = await GetBasePackageAsync(context.CancellationToken);
response.Value = GetConfigurationValue(request.Key, package);
response.Description = GetConfigurationDescription(request.Key);
_logger.LogDebug("Configuration requested: Key={Key}, Value={Value}", request.Key, response.Value);
return Task.FromResult(response);
return response;
}
/// <summary>
/// دریافت تنظیمات باشگاه مشتریان
/// </summary>
public override Task<GetClubConfigurationResponse> GetClubConfiguration(Empty request, ServerCallContext context)
public override async Task<GetClubConfigurationResponse> GetClubConfiguration(Empty request, ServerCallContext context)
{
var package = await GetBasePackageAsync(context.CancellationToken);
var response = new GetClubConfigurationResponse
{
ActivationFee = SystemConstants.ClubActivationFee,
MembershipGiftValue = SystemConstants.ClubMembershipGiftValue
ActivationFee = package.ActivationFee,
MembershipGiftValue = package.ActivationFee
};
_logger.LogDebug("Club configuration requested: ActivationFee={ActivationFee}, GiftValue={GiftValue}",
response.ActivationFee, response.MembershipGiftValue);
return Task.FromResult(response);
return response;
}
/// <summary>
@@ -111,18 +119,19 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
/// دریافت تمام تنظیمات
/// </summary>
[RequiresPermission(PermissionNames.SettingsView)]
public override Task<GetAllConfigurationsResponse> GetAllConfigurations(GetAllConfigurationsRequest request, ServerCallContext context)
public override async Task<GetAllConfigurationsResponse> GetAllConfigurations(GetAllConfigurationsRequest request, ServerCallContext context)
{
var package = await GetBasePackageAsync(context.CancellationToken);
var response = new GetAllConfigurationsResponse();
// Club Settings
response.Models.Add(CreateConfigModel("Club.ActivationFee", SystemConstants.ClubActivationFee.ToString(), "هزینه فعال‌سازی عضویت باشگاه", 2));
response.Models.Add(CreateConfigModel("Club.MembershipGiftValue", SystemConstants.ClubMembershipGiftValue.ToString(), "مبلغ هدیه حق عضویت باشگاه", 2));
// Club Settings (از Package)
response.Models.Add(CreateConfigModel("Club.ActivationFee", package.ActivationFee.ToString(), "هزینه فعال‌سازی عضویت باشگاه", 2));
response.Models.Add(CreateConfigModel("Club.MembershipGiftValue", package.ActivationFee.ToString(), "مبلغ هدیه حق عضویت باشگاه", 2));
// Commission Settings
// Commission Settings (سقف‌ها از Package)
response.Models.Add(CreateConfigModel("Commission.MinWithdrawalAmount", SystemConstants.CommissionMinWithdrawalAmount.ToString(), "حداقل مبلغ برداشت", 3));
response.Models.Add(CreateConfigModel("Commission.MaxWeeklyBalancesPerLeg", SystemConstants.CommissionMaxWeeklyBalancesPerLeg.ToString(), "سقف تعادل هفتگی برای هر دست", 3));
response.Models.Add(CreateConfigModel("Commission.MaxNetworkLevel", SystemConstants.CommissionMaxNetworkLevel.ToString(), "حداکثر عمق شبکه برای محاسبه کمیسیون", 3));
response.Models.Add(CreateConfigModel("Commission.MaxWeeklyBalancesPerLeg", package.MaxBalancesPerLeg.ToString(), "سقف تعادل هفتگی برای هر دست", 3));
response.Models.Add(CreateConfigModel("Commission.MaxNetworkLevel", package.MaxNetworkLevel.ToString(), "حداکثر عمق شبکه برای محاسبه کمیسیون", 3));
response.Models.Add(CreateConfigModel("Commission.CashWithdrawalEnabled", SystemConstants.CommissionCashWithdrawalEnabled.ToString(), "امکان برداشت نقدی", 3));
response.Models.Add(CreateConfigModel("Commission.CalculationStrategy", SystemConstants.CommissionCalculationStrategy, "روش محاسبه کمیسیون", 3));
@@ -130,9 +139,14 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
response.Models.Add(CreateConfigModel("Network.AllowOrphanNodes", SystemConstants.NetworkAllowOrphanNodes.ToString(), "اجازه حذف والدین با فرزند", 1));
response.Models.Add(CreateConfigModel("Network.MaxChildrenPerLeg", SystemConstants.NetworkMaxChildrenPerLeg.ToString(), "حداکثر تعداد فرزند مستقیم در هر پا", 1));
// Package Settings
response.Models.Add(CreateConfigModel("Package.BasePackageAmount", SystemConstants.BasePackageAmount.ToString(), "مبلغ پکیج پایه", 0));
response.Models.Add(CreateConfigModel("Package.DayaLoanAmount", SystemConstants.DayaLoanAmount.ToString(), "مبلغ وام دایا", 0));
// Package Settings (از Package)
response.Models.Add(CreateConfigModel("Package.BasePackageAmount", package.Price.ToString(), "مبلغ پکیج پایه", 0));
response.Models.Add(CreateConfigModel("Package.DayaLoanAmount", package.Price.ToString(), "مبلغ وام دایا", 0));
// Magic Wallet Settings (از Package)
response.Models.Add(CreateConfigModel("MagicWallet.Multiplier", package.MagicWalletMultiplier.ToString(), "ضریب شارژ کیف‌پول جادویی", 0));
response.Models.Add(CreateConfigModel("MagicWallet.MaxDeposit", package.MagicWalletMaxDeposit.ToString(), "سقف واریز هر دور جادویی", 0));
response.Models.Add(CreateConfigModel("MagicWallet.MaxCredit", package.MagicWalletMaxCredit.ToString(), "سقف اعتبار هر دور جادویی", 0));
// System Settings
response.Models.Add(CreateConfigModel("System.MaintenanceMode", SystemConstants.SystemMaintenanceMode.ToString(), "حالت تعمیر و نگهداری", 0));
@@ -141,7 +155,7 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
// Shop Settings
response.Models.Add(CreateConfigModel("Shop.VAT", SystemConstants.ShopVAT.ToString(), "مالیات بر ارزش افزوده", 0));
return Task.FromResult(response);
return response;
}
/// <summary>
@@ -154,36 +168,43 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
throw new RpcException(new Status(StatusCode.FailedPrecondition, "تنظیمات سیستم فقط خواندنی هستند. تغییر از طریق کد انجام می‌شود"));
}
#region [ARCHIVED] DeactivateConfiguration & GetConfigurationHistory SystemConstants ثابت هستند، این RPCها بیمعنی
[RequiresPermission(PermissionNames.SettingsManageConfiguration)]
[Obsolete("ARCHIVED: SystemConstants are read-only — deactivation is meaningless")]
public override Task<Empty> DeactivateConfiguration(DeactivateConfigurationRequest request, ServerCallContext context)
{
_logger.LogWarning("DeactivateConfiguration called but SystemConstants are read-only. Key: {Key}", request.Key);
_logger.LogWarning("[ARCHIVED] DeactivateConfiguration called but SystemConstants are read-only. Key: {Key}", request.Key);
throw new RpcException(new Status(StatusCode.FailedPrecondition, "تنظیمات سیستم فقط خواندنی هستند. غیرفعال‌سازی از طریق کد انجام می‌شود"));
}
[Obsolete("ARCHIVED: SystemConstants have no history — always returns empty")]
public override Task<GetConfigurationHistoryResponse> GetConfigurationHistory(GetConfigurationHistoryRequest request, ServerCallContext context)
{
// چون constant هستند، تاریخچه‌ای وجود نداره
// [ARCHIVED] چون constant هستند، تاریخچه‌ای وجود نداره
return Task.FromResult(new GetConfigurationHistoryResponse());
}
#endregion
#region Private Helpers
private static string GetConfigurationValue(string key)
private static string GetConfigurationValue(string key, Package? package)
{
return key switch
{
"Club.ActivationFee" => SystemConstants.ClubActivationFee.ToString(),
"Club.MembershipGiftValue" => SystemConstants.ClubMembershipGiftValue.ToString(),
"Club.ActivationFee" => (package?.ActivationFee ?? 0).ToString(),
"Club.MembershipGiftValue" => (package?.ActivationFee ?? 0).ToString(),
"Commission.MinWithdrawalAmount" => SystemConstants.CommissionMinWithdrawalAmount.ToString(),
"Commission.MaxWeeklyBalancesPerLeg" => SystemConstants.CommissionMaxWeeklyBalancesPerLeg.ToString(),
"Commission.MaxNetworkLevel" => SystemConstants.CommissionMaxNetworkLevel.ToString(),
"Commission.MaxWeeklyBalancesPerLeg" => (package?.MaxBalancesPerLeg ?? 0).ToString(),
"Commission.MaxNetworkLevel" => (package?.MaxNetworkLevel ?? 0).ToString(),
"Commission.CashWithdrawalEnabled" => SystemConstants.CommissionCashWithdrawalEnabled.ToString(),
"Commission.CalculationStrategy" => SystemConstants.CommissionCalculationStrategy,
"Network.AllowOrphanNodes" => SystemConstants.NetworkAllowOrphanNodes.ToString(),
"Network.MaxChildrenPerLeg" => SystemConstants.NetworkMaxChildrenPerLeg.ToString(),
"Package.BasePackageAmount" => SystemConstants.BasePackageAmount.ToString(),
"Package.DayaLoanAmount" => SystemConstants.DayaLoanAmount.ToString(),
"Package.BasePackageAmount" => (package?.Price ?? 0).ToString(),
"Package.DayaLoanAmount" => (package?.Price ?? 0).ToString(),
"MagicWallet.Multiplier" => (package?.MagicWalletMultiplier ?? 0).ToString(),
"MagicWallet.MaxDeposit" => (package?.MagicWalletMaxDeposit ?? 0).ToString(),
"MagicWallet.MaxCredit" => (package?.MagicWalletMaxCredit ?? 0).ToString(),
"System.MaintenanceMode" => SystemConstants.SystemMaintenanceMode.ToString(),
"System.EnableAuditLog" => SystemConstants.SystemEnableAuditLog.ToString(),
"Shop.VAT" => SystemConstants.ShopVAT.ToString(),
@@ -191,6 +212,13 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
};
}
private async Task<Package> GetBasePackageAsync(CancellationToken cancellationToken)
{
return await _context.Packages
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
?? throw new RpcException(new Status(StatusCode.Internal, "پکیج پایه یافت نشد"));
}
private static string GetConfigurationDescription(string key)
{
return key switch
@@ -206,6 +234,9 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
"Network.MaxChildrenPerLeg" => "حداکثر تعداد فرزند مستقیم در هر پا",
"Package.BasePackageAmount" => "مبلغ پکیج پایه (ریال)",
"Package.DayaLoanAmount" => "مبلغ وام دایا (ریال)",
"MagicWallet.Multiplier" => "ضریب شارژ کیف‌پول جادویی",
"MagicWallet.MaxDeposit" => "سقف واریز هر دور جادویی (ریال)",
"MagicWallet.MaxCredit" => "سقف اعتبار هر دور جادویی (ریال)",
"System.MaintenanceMode" => "حالت تعمیر و نگهداری سیستم",
"System.EnableAuditLog" => "فعال‌سازی لاگ تغییرات",
"Shop.VAT" => "مالیات بر ارزش افزوده",
@@ -25,9 +25,11 @@ public class HealthService : HealthContract.HealthContractBase
return result.Adapt<GetSystemHealthResponse>();
}
#region [ARCHIVED] GetServiceHealth تکراری: GetSystemHealth کل سیستم را برمیگرداند، فیلتر client-side کافی است
[Obsolete("ARCHIVED: Use GetSystemHealth + client-side filter by service name")]
public override async Task<GetServiceHealthResponse> GetServiceHealth(GetServiceHealthRequest request, ServerCallContext context)
{
// For now, just return system health filtered by service name
// [ARCHIVED] duplicate — GetSystemHealth returns all, client can filter
var systemHealthQuery = new GetSystemHealthQuery();
var systemHealth = await _mediator.Send(systemHealthQuery, context.CancellationToken);
@@ -45,4 +47,5 @@ public class HealthService : HealthContract.HealthContractBase
CheckedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.UtcNow)
};
}
#endregion
}
@@ -3,10 +3,6 @@ using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.PackageCQ.Commands.CreateNewPackage;
using CMSMicroservice.Application.PackageCQ.Commands.UpdatePackage;
using CMSMicroservice.Application.PackageCQ.Commands.DeletePackage;
using CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
using CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
using CMSMicroservice.Application.PackageCQ.Commands.InitiateBasePackagePayment;
using CMSMicroservice.Application.PackageCQ.Commands.VerifyBasePackagePayment;
using CMSMicroservice.Application.PackageCQ.Queries.GetPackage;
using CMSMicroservice.Application.PackageCQ.Queries.GetAllPackageByFilter;
using CMSMicroservice.Application.PackageCQ.Queries.GetUserPackageStatus;
@@ -72,32 +68,11 @@ public class PackageService : PackageContract.PackageContractBase
return await _dispatchRequestToCQRS.Handle<GetAllPackageByFilterRequest, GetAllPackageByFilterQuery, GetAllPackageByFilterResponse>(request, context);
}
public override async Task<PurchaseGoldenPackageResponse> PurchaseGoldenPackage(PurchaseGoldenPackageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<PurchaseGoldenPackageRequest, PurchaseGoldenPackageCommand, PurchaseGoldenPackageResponse>(request, context);
}
public override async Task<VerifyGoldenPackagePurchaseResponse> VerifyGoldenPackagePurchase(VerifyGoldenPackagePurchaseRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<VerifyGoldenPackagePurchaseRequest, VerifyGoldenPackagePurchaseCommand, VerifyGoldenPackagePurchaseResponse>(request, context);
}
public override async Task<GetUserPackageStatusResponse> GetUserPackageStatus(GetUserPackageStatusRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserPackageStatusRequest, GetUserPackageStatusQuery, GetUserPackageStatusResponse>(request, context);
}
// Base Package Payment (56M)
public override async Task<InitiateBasePackagePaymentResponse> InitiateBasePackagePayment(InitiateBasePackagePaymentRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<InitiateBasePackagePaymentRequest, InitiateBasePackagePaymentCommand, InitiateBasePackagePaymentResponse>(request, context);
}
public override async Task<VerifyBasePackagePaymentResponse> VerifyBasePackagePayment(VerifyBasePackagePaymentRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<VerifyBasePackagePaymentRequest, VerifyBasePackagePaymentCommand, VerifyBasePackagePaymentResponse>(request, context);
}
// ============= Customer-specific Method Implementations =============
public override async Task<GetCustomerPackagesResponse> GetCustomerPackages(GetCustomerPackagesRequest request, ServerCallContext context)
@@ -207,13 +182,17 @@ public class PackageService : PackageContract.PackageContractBase
.Select(u => new { u.Mobile })
.FirstOrDefaultAsync(context.CancellationToken);
// Embed orderId in callback URL so FrontOffice can pass it back for verification
var separator = request.CallbackUrl.Contains('?') ? "&" : "?";
var callbackWithOrder = $"{request.CallbackUrl}{separator}orderId={purchase.Id}";
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
Amount = package.Price,
UserId = userId,
Mobile = user?.Mobile ?? string.Empty,
Description = $"خرید پکیج {package.Title}",
CallbackUrl = request.CallbackUrl
CallbackUrl = callbackWithOrder
}, context.CancellationToken);
if (!paymentResult.IsSuccess)
@@ -317,6 +296,48 @@ public class PackageService : PackageContract.PackageContractBase
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Success;
transaction.PaymentDate = DateTime.UtcNow;
transaction.RefId = verifyResult.RefId;
// شارژ کیف پول کاربر
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == purchase.UserId, context.CancellationToken);
if (wallet == null)
{
wallet = new CMSMicroservice.Domain.Entities.UserWallet
{
UserId = purchase.UserId,
Balance = 0,
DiscountBalance = 0,
NetworkBalance = 0
};
_context.UserWallets.Add(wallet);
await _context.SaveChangesAsync(context.CancellationToken);
}
var discountAmount = (long)(purchase.Amount * (double)(purchase.Package?.DiscountMultiplier ?? 2.0m));
wallet.Balance += purchase.Amount;
wallet.DiscountBalance += discountAmount;
// ثبت لاگ کیف پول
var walletLog = new CMSMicroservice.Domain.Entities.UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = purchase.Amount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = discountAmount,
IsIncrease = true,
RefrenceId = transaction.Id
};
_context.UserWalletChangeLogs.Add(walletLog);
// به‌روزرسانی کاربر
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Id == purchase.UserId, context.CancellationToken);
if (user != null)
user.PackagePurchaseMethod = Domain.Enums.PackagePurchaseMethod.DirectPurchase;
}
else if (transaction != null)
{
@@ -344,9 +344,18 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
.Include(u => u.ClubMembership)
.FirstOrDefaultAsync(u => u.Id == userId, context.CancellationToken);
if (wallet.WalletMode == WalletMode.Magic
&& wallet.MagicTotalDeposited >= SystemConstants.MagicWalletMaxDeposit)
if (wallet.WalletMode == WalletMode.Magic)
{
// بارگذاری پکیج کاربر برای سقف کیف‌پول جادویی
var userCycle = await _context.ClubMembershipCycles
.FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, context.CancellationToken);
var userPackage = userCycle != null
? await _context.Packages.FirstOrDefaultAsync(p => p.Id == userCycle.PackageId, context.CancellationToken)
: await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, context.CancellationToken);
var magicMaxDeposit = userPackage?.MagicWalletMaxDeposit ?? 1_000_000_000;
if (wallet.MagicTotalDeposited >= magicMaxDeposit)
{
// ── EXIT Magic Mode ──
// هر دو شرط: Balance=0 و سقف 100M پر شده
wallet.WalletMode = WalletMode.Normal;
@@ -358,6 +367,14 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
if (currentCycle != null)
{
currentCycle.MagicCompletedAt = DateTime.UtcNow;
currentCycle.IsCurrentCycle = false;
}
// B6 fix: ریست PackagePurchaseMethod تا Guards G1-G3 خرید مجدد را مسدود نکنند
if (user != null)
{
user.PackagePurchaseMethod = PackagePurchaseMethod.None;
}
}
}
else if (wallet.WalletMode == WalletMode.Normal
@@ -497,7 +514,9 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
};
}
#region [ARCHIVED] GetOrdersByDateRange تکراری: GetAllUserOrderByFilter همین قابلیت + فیلترهای بیشتر دارد
[RequiresPermission(PermissionNames.ReportsView)]
[Obsolete("ARCHIVED: Use GetAllUserOrderByFilter instead — has date range + more filters")]
public override async Task<GetOrdersByDateRangeResponse> GetOrdersByDateRange(GetOrdersByDateRangeRequest request, ServerCallContext context)
{
var pageNumber = request.PageNumber > 0 ? request.PageNumber : 1;
@@ -551,6 +570,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
Orders = { orders }
};
}
#endregion
[RequiresPermission(PermissionNames.OrdersUpdate)]
public override async Task<ApplyDiscountToOrderResponse> ApplyDiscountToOrder(ApplyDiscountToOrderRequest request, ServerCallContext context)
@@ -624,17 +644,21 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
// ============= Customer-specific Methods =============
#region [ARCHIVED] CreateNewOrderForCustomer & SubmitOrderForCustomer Stub خالی: مسیر سفارش مشتری از SubmitShopBuyOrder میگذرد
[Obsolete("ARCHIVED: Empty stub — customer order flow uses SubmitShopBuyOrder")]
public override async Task<CreateNewUserOrderResponse> CreateNewOrderForCustomer(CreateNewUserOrderRequest request, ServerCallContext context)
{
// For now, return empty response - will be implemented properly later
// [ARCHIVED] Empty stub — مسیر سفارش مشتری از SubmitShopBuyOrder می‌گذرد
return new CreateNewUserOrderResponse();
}
[Obsolete("ARCHIVED: Empty stub — customer order flow uses SubmitShopBuyOrder")]
public override async Task<SubmitShopBuyOrderResponse> SubmitOrderForCustomer(SubmitShopBuyOrderRequest request, ServerCallContext context)
{
// For now, return empty response - will be implemented properly later
// [ARCHIVED] Empty stub — مسیر سفارش مشتری از SubmitShopBuyOrder می‌گذرد
return new SubmitShopBuyOrderResponse();
}
#endregion
public override async Task<GetAllUserOrderByFilterResponse> GetCustomerOrders(GetAllUserOrderByFilterRequest request, ServerCallContext context)
{
@@ -227,13 +227,21 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
var cycleCount = await _context.ClubMembershipCycles
.CountAsync(c => c.UserId == userId, context.CancellationToken);
// بارگذاری پکیج کاربر برای سقف کیف‌پول جادویی
var currentCycle = await _context.ClubMembershipCycles
.FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, context.CancellationToken);
var package = currentCycle != null
? await _context.Packages.FirstOrDefaultAsync(p => p.Id == currentCycle.PackageId, context.CancellationToken)
: await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, context.CancellationToken);
var magicMaxDeposit = package?.MagicWalletMaxDeposit ?? 1_000_000_000;
var response = new GetMagicWalletStatusResponse
{
WalletMode = (int)wallet.WalletMode,
MagicTotalDeposited = wallet.MagicTotalDeposited,
MagicTotalCredited = wallet.MagicTotalCredited,
MagicMaxDeposit = SystemConstants.MagicWalletMaxDeposit,
MagicRemainingDeposit = Math.Max(0, SystemConstants.MagicWalletMaxDeposit - wallet.MagicTotalDeposited),
MagicMaxDeposit = magicMaxDeposit,
MagicRemainingDeposit = Math.Max(0, magicMaxDeposit - wallet.MagicTotalDeposited),
Balance = wallet.Balance,
PurchaseCycleCount = cycleCount
};
@@ -0,0 +1,90 @@
-- =============================================
-- Data Migration: Package-Based System
-- Date: 2026-02-25
-- Description: Seeds golden package and backfills
-- existing records with PackageId = 1
-- NOTE: This is automatically executed in the EF Migration
-- (AddPackageBasedSystem). This file is for reference only.
-- =============================================
USE [YourDatabaseName]; -- Replace with actual DB name
GO
-- 1. Seed Golden Package (Id=1)
SET IDENTITY_INSERT [CMS].[Packages] ON;
IF NOT EXISTS (SELECT 1 FROM [CMS].[Packages] WHERE [Id] = 1)
BEGIN
INSERT INTO [CMS].[Packages]
([Id], [Title], [Description], [ImagePath], [Price],
[SortOrder], [IsActive], [IsBasePackage],
[SupportsDayaPurchase], [SupportsDirectPurchase],
[ActivationFee], [DiscountMultiplier], [MagicWalletMultiplier],
[MaxBalancesPerLeg], [MaxNetworkLevel],
[MagicWalletMaxDeposit], [MagicWalletMaxCredit],
[Created], [IsDeleted])
VALUES
(1, N'پکیج طلایی', N'پکیج اصلی باشگاه مشتریان کارا بازار سلامت',
N'/images/packages/golden.png', 56000000,
1, 1, 1,
1, 1,
25200000, 2.0, 2.5,
300, 15,
1000000000, 2500000000,
GETUTCDATE(), 0);
PRINT 'Golden Package (Id=1) seeded successfully.';
END
ELSE
BEGIN
PRINT 'Golden Package (Id=1) already exists. Skipping seed.';
END
SET IDENTITY_INSERT [CMS].[Packages] OFF;
GO
-- 2. Backfill PackageId on commission/network tables
UPDATE [CMS].[WeeklyCommissionPools] SET [PackageId] = 1 WHERE [PackageId] = 0;
PRINT CONCAT('WeeklyCommissionPools updated: ', @@ROWCOUNT, ' rows');
UPDATE [CMS].[UserCommissionPayouts] SET [PackageId] = 1 WHERE [PackageId] = 0;
PRINT CONCAT('UserCommissionPayouts updated: ', @@ROWCOUNT, ' rows');
UPDATE [CMS].[NetworkWeeklyBalances] SET [PackageId] = 1 WHERE [PackageId] = 0;
PRINT CONCAT('NetworkWeeklyBalances updated: ', @@ROWCOUNT, ' rows');
UPDATE [CMS].[ClubMembershipCycles] SET [PackageId] = 1 WHERE [PackageId] = 0;
PRINT CONCAT('ClubMembershipCycles updated: ', @@ROWCOUNT, ' rows');
GO
-- 3. Backfill ClubMembership First/Last Activation from existing ActivatedAt
UPDATE cm SET
cm.[FirstActivationDate] = cm.[ActivatedAt],
cm.[LastActivationDate] = cm.[ActivatedAt],
cm.[FirstPackageId] = 1,
cm.[LastPackageId] = 1
FROM [CMS].[ClubMemberships] cm
WHERE cm.[ActivatedAt] IS NOT NULL
AND cm.[FirstActivationDate] IS NULL;
PRINT CONCAT('ClubMemberships backfilled: ', @@ROWCOUNT, ' rows');
GO
-- 4. Verification Queries
SELECT 'Packages' AS [Table], COUNT(*) AS [Total],
SUM(CASE WHEN [IsBasePackage] = 1 THEN 1 ELSE 0 END) AS [BasePackages]
FROM [CMS].[Packages] WHERE [IsDeleted] = 0;
SELECT 'WeeklyCommissionPools' AS [Table],
COUNT(*) AS [Total],
SUM(CASE WHEN [PackageId] = 0 THEN 1 ELSE 0 END) AS [MissingPackageId]
FROM [CMS].[WeeklyCommissionPools] WHERE [IsDeleted] = 0;
SELECT 'ClubMemberships' AS [Table],
COUNT(*) AS [Total],
SUM(CASE WHEN [FirstPackageId] IS NOT NULL THEN 1 ELSE 0 END) AS [WithFirstPackage],
SUM(CASE WHEN [LastActivationDate] IS NOT NULL THEN 1 ELSE 0 END) AS [WithLastActivation]
FROM [CMS].[ClubMemberships] WHERE [IsDeleted] = 0;
PRINT 'Data migration completed successfully.';
GO