45 Commits

Author SHA1 Message Date
masoodafar-web fb81aff095 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 8m48s
# Conflicts:
#	.gitea/workflows/kub-deploy.yml
#	k8s/staging/cms-deployment.yaml
#	src/CMSMicroservice.Application/DiscountShopCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommandHandler.cs
#	src/CMSMicroservice.Application/DiscountShopCQ/Commands/PlaceOrder/PlaceOrderCommandHandler.cs
#	src/CMSMicroservice.Application/WalletCQ/Commands/VerifyDiscountWalletCharge/VerifyDiscountWalletChargeCommandHandler.cs
#	src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommandHandler.cs
#	src/CMSMicroservice.Infrastructure/Services/Payment/ZarinPalPaymentService.cs
#	src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs
#	src/CMSMicroservice.WebApi/appsettings.Production.json
#	src/CMSMicroservice.WebApi/appsettings.Staging.json
#	src/CMSMicroservice.WebApi/appsettings.json
2026-02-28 04:51:57 +03:30
masoodafar-web 1a0bb83c99 feat: Enhance SMS verification with new token handling and update API key
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m45s
2026-02-28 04:01:45 +03:30
masoodafar-web 1e7c17f090 feat: Update payment processing and callback mechanisms
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m22s
- Refactor ActivateClubMembershipCommandHandler to use UserPackagePurchases instead of UserOrders for package activation.
- Modify PlaceOrderCommandHandler to redirect payment callbacks to the front office.
- Update ChargeDiscountWalletCommandHandler and ChargeMagicWalletCommandHandler to direct payment callbacks to the front office.
- Remove PaymentCallbackController and integrate payment verification directly into DiscountOrderService and UserWalletService.
- Add CustomerVerifyDiscountOrderPayment RPC to DiscountOrderService for verifying discount order payments.
- Implement VerifyMagicCharge and VerifyDiscountCharge methods in UserWalletService for wallet charge verifications.
- Update appsettings.json to use local URLs for development.
- Remove appsettings.Development.json as it is no longer needed.
- Comment out history tracking methods in ClubMembershipCycle and Package classes.
- Update PackageService to automatically activate club membership after successful payment verification.
- Adjust UserService to generate JWT tokens with user details.
2026-02-28 03:27:46 +03:30
masoodafar-web 01073084ab security: remove callback URL from user input — read from FrontOfficeBaseUrl config
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m28s
- PackageService: build callback URL from FrontOfficeBaseUrl config instead of request.CallbackUrl
- TransactionsService: same fix — callback URL from config
- Prevents potential open redirect vulnerability (user could send arbitrary URL)

All callback URLs now come from appsettings FrontOfficeBaseUrl or CmsBaseUrl.
2026-02-27 22:20:04 +03:30
masoodafar-web ed2b20a98e fix: wallet callback URL issues
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- PaymentCallbackController: fix redirect paths /magic-wallet → /profile/magic-wallet
- Add appsettings.Development.json with local CmsBaseUrl (localhost:32846) and FrontOfficeBaseUrl (localhost:5268)
- Proto: fix comments ریال → تومان in wallet charge messages

Root cause: CmsBaseUrl in appsettings.json points to stage (cms.se.kbs1.ir),
so when testing locally, ZarinPal callback went to stage CMS instead of local.
Also redirect after verify used wrong FO path (/magic-wallet instead of /profile/magic-wallet).
2026-02-27 22:09:15 +03:30
masoodafar-web 721661af0f fix: pass amount to ZarinPal VerifyPaymentAsync — fix verify failure (amount=0 bug)
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m38s
- PackageService.CustomerVerifyPackagePurchase: look up PaymentTransaction.Amount and pass to 3-arg overload
- TransactionsService.CustomerPaymentVerification: same fix
- VerifyDiscountWalletChargeCommandHandler: look up amount from PaymentTransaction
- VerifyPackagePurchaseCommandHandler: fix copy-paste bug (Authority as verificationToken) + add amount
- IPaymentGatewayService: throw NotImplementedException in default 3-arg impl to prevent silent amount=0
- MockPaymentGatewayService & DayaPaymentService: add 3-arg overload for compatibility

Root cause: ZarinPal requires the exact amount in verify request.
The 2-arg overload was sending amount=0 which caused Code=-1 (تأیید تراکنش ناموفق).
2026-02-27 21:17:50 +03:30
masoodafar-web aee7b59fc1 fix: Toman/Rial — system uses Toman everywhere, only ZarinPal gets Rial
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 10m16s
Core change in ZarinPalPaymentService:
- InitiatePaymentAsync: Amount (Toman) × 10 → Rial for gateway
- VerifyPaymentWithAmountAsync: Amount (Toman) × 10 → Rial for verify
- Verify result: gateway Rial ÷ 10 → Toman back to system

Also fixed:
- Package.Price doc: ریال → تومان
- Error messages in 3 handlers: ریال → تومان
- ConfigurationService descriptions: ریال → تومان (7 entries)
- Notification SMS/email: ریال → تومان
- Doc comments in 5 command files: ریال → تومان
- PaymentCallback comments: ریال → تومان

14 files changed across Domain, Application, Infrastructure, WebApi
2026-02-27 21:03:27 +03:30
masoodafar-web 61b7e4f8f2 F2-F7: نوتیفیکیشن‌ها+نام‌پکیج، ستون پکیج در CSV، مقادیر داینامیک کیف‌پول جادویی، ولیدیتور SystemConstants
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m29s
F2: اضافه شدن packageName به SmsTemplates، IUserNotificationService، UserNotificationService
    - DayaLoan، ClubActivated، CommissionDeposited حالا نام پکیج را نشان می‌دهند
F4: اضافه شدن ستون پکیج به CSV خروجی‌ها
    - commission.proto: package_name در WithdrawalRequestModel
    - manualpayment.proto: package_name در ManualPaymentModel
    - کوئری‌هندلرها UserPackagePurchases لوکاپ اضافه شد
F6: مقادیر داینامیک کیف‌پول جادویی از پکیج
    - userwallet.proto: magic_multiplier + magic_max_credit
    - UserWalletService: پاپیولیت فیلدهای جدید + فالبک به SystemConstants
F7: ولیدیتورها از SystemConstants استفاده می‌کنند
    - WalletMaxSafeAmount (10B ریال) به عنوان حصار ایمنی
    - MagicWalletMinCharge، DiscountWalletMinCharge ثابت‌های مرکزی
    - سقف واقعی per-package در هندلرها اعمال می‌شود
Proto: v0.0.189
2026-02-27 08:47:46 +03:30
masoodafar-web e5bc3a952a feat: ClubMembershipCycle IHasHistory + PackageId in WalletHistory (F1)
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m33s
- ClubMembershipCycle now implements IHasHistory<ClubMembershipCycleHistory>
  - CreateHistorySnapshot: New* fields from current state, Old* auto-filled by interceptor
  - Parses action string to ClubMembershipCycleAction enum

- Add PackageId to 6 UserWalletHistory creation sites (F1 fix):
  - VerifyPackagePurchaseCommandHandler: balanceLog + discountLog (order.PackageId)
  - CheckAndProcessDayaLoansCommandHandler: mainLog + discountLog (package.Id)
  - CreateManualPaymentCommandHandler: walletLog (package.Id)
  - PackageService.CustomerVerifyPackagePurchase: walletLog (purchase.PackageId)
  - Non-package flows (orders, commissions, manual) correctly keep PackageId=null
2026-02-27 07:07:30 +03:30
masoodafar-web 10d2ca20d1 refactor: rename UserWalletChangeLog→UserWalletHistory, add History interceptor & migration
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m11s
- Rename UserWalletChangeLog to UserWalletHistory across 54+ files (entities, configs, DTOs, commands, queries, protos, services)
- Rename 34 files and 11 directories accordingly
- Rename proto file userwalletchangelog.proto → userwallethistory.proto
- Add IHasHistory<T> generic interface for history auto-tracking
- Implement IHasHistory<PackageHistory> on Package entity
- Add HistoryTrackingSaveChangesInterceptor (reflection-based, auto-fills Old* values from OriginalValues)
- Wire interceptor in DI and ApplicationDbContext
- Add EF migration Q27_HistoryTables_And_RenameWalletHistory:
  * RenameTable UserWalletChangeLogs → UserWalletHistories (preserves data)
  * Rename PK, FK constraints and indexes via sp_rename
  * CreateTable ClubMembershipCycleHistories + PackageHistories
2026-02-27 06:22:15 +03:30
masoodafar-web fdbb91d2e1 feat(Q27): PackageHistory + ClubMembershipCycleHistory entities
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m28s
Q27: History Tables standardization — added missing history entities:
- PackageHistory: tracks price, activation fee, magic settings changes
- ClubMembershipCycleHistory: tracks cycle state transitions
- PackageAction + ClubMembershipCycleAction enums
- EF configurations with proper indexes and FK relationships
- DbSets registered in IApplicationDbContext + ApplicationDbContext
- Navigation properties on Package + ClubMembershipCycle entities
2026-02-27 05:31:21 +03:30
masoodafar-web a1024a3e18 feat(Q24+Q26): balance threshold + SP auto-deploy worker
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m25s
Q24: Magic wallet entry/exit trigger now uses Balance <= 1,000,000 Rials
  instead of Balance == 0 (products have varying prices so exact zero
  is unreachable). Added MagicWalletEntryThreshold to SystemConstants.

Q26: StoredProcedureDeploymentService (IHostedService) auto-deploys
  stored procedures on startup via embedded SQL resources + SHA256
  checksum comparison. Creates [CMS].[__StoredProcedureVersions] table
  automatically. Supports GO batch separators.
2026-02-27 04:22:24 +03:30
masoodafar-web 1ac23667e2 fix: package-based compliance audit — feature DIFF, remove hardcoded fallbacks
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m12s
- ActivateClubMembership: replace GetAllFeatureIds with PackageFeature DIFF (Q20)
- ActivateClubMembership: fix re-activation early return — check current cycle
- AcceptClubMembershipContract: same feature DIFF logic
- VerifyPackagePurchase: remove ?? 2.0m fallback, throw if package null
- PackageService: remove ?? 2.0m fallback for DiscountMultiplier
- UserOrderService: remove ?? 1B fallback for MagicWalletMaxDeposit
- SystemConstants: remove 9 dead [Obsolete] constants (per-package values)
2026-02-27 02:35:41 +03:30
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
masoodafar-web cbaa20f339 fix: remove double ×10 Rial conversion in ZarinPalPaymentService
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
Root cause: FrontOffice already converts Toman→Rial (×10) before sending
to CMS. ZarinPalPaymentService was multiplying by 10 AGAIN, causing
amounts to be 10x too large.

Example: user enters 500K Toman → FO sends 5M Rial → CMS did ×10 → 50M
Rial sent to ZarinPal → showed 5M Toman instead of 500K.

Changes:
- ZarinPalPaymentService.InitiatePaymentAsync: remove ×10 (amount already Rial)
- ZarinPalPaymentService.VerifyPaymentWithAmountAsync: remove ×10 + accept Rial
- ZarinPalPaymentService.VerifyResult.Amount: return Rial (no /10 conversion)
- VerifyMagicWalletChargeCommandHandler: remove /10 before calling Verify
- PaymentCallbackController: update comments (amount is Rial)
- Also includes: improved HTTP error logging for non-200 responses
- Also includes: production URL fix (kbs1→kbs2)
2026-02-24 00:49:16 +03:30
masoodafar-web e206b71186 fix: reduce discount order expiry from 30 to 15 minutes
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-02-24 00:24:02 +03:30
masoodafar-web 0457ef6c7c fix: validate discount wallet balance before applying discount in PlaceOrderCommandHandler
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m36s
2026-02-23 23:06:37 +03:30
masoodafar-web f3ac5ad7df fix: add missing UserWalletChangeLog for discount shop purchases and discount wallet charge
Bug: when buying from discount shop, DiscountBalance was deducted from DB
but no UserWalletChangeLog was created — making it invisible in wallet history.
Same issue existed for discount wallet top-up (charge).

Fixed in 3 handlers:
- PlaceOrderCommandHandler (fully paid by discount balance path)
- CompleteOrderPaymentCommandHandler (gateway + discount balance path)
- VerifyDiscountWalletChargeCommandHandler (discount wallet charge)
2026-02-23 22:59:00 +03:30
masoodafar-web e72673c184 chore(staging): remove appsettings.Production.json — config comes from K8s Secret
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m42s
DO NOT cherry-pick this commit to production branch
2026-02-23 22:07:58 +03:30
masoodafar-web 9288d0640b feat: externalize appsettings to K8s Secret — config persists independently of image rebuilds
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m57s
- Add cms-config.yaml (K8s Secret) for staging and production
- Mount appsettings.{Environment}.json from Secret into /app/
- Update CI/CD pipelines to apply Secret before Deployment
- Remove redundant env vars (Kestrel, FileStorage) — now in config file
2026-02-23 21:58:05 +03:30
masoodafar-web de83c31346 fix: production uses namespace default + remove foursat namespace references
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m18s
2026-02-23 21:36:08 +03:30
masoodafar-web f8dc4abd04 fix: staging ASPNETCORE_ENVIRONMENT=Staging, remove secretKeyRef (config in appsettings)
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m28s
2026-02-23 21:17:23 +03:30
masoodafar-web 2d6c95e6ee fix: use local registry 194.5.195.53:30080 instead of git.se.kbs1.ir for image pull
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 11m28s
2026-02-23 20:58:16 +03:30
masoodafar-web e41747afe0 fix: production ingress — add cms.kbs2.ir, use ingressClassName instead of annotation
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 12m6s
2026-02-23 20:41:12 +03:30
masoodafar-web 68da3f45a3 fix: staging uses namespace default, not foursat
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-02-23 20:38:27 +03:30
masoodafar-web 3153fd8a74 feat: add PersistentVolume for CMS uploads + apply manifests in CI/CD
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 12m12s
- Add k8s/staging/ and k8s/production/ deployment manifests
- PVC 20Gi for /app/Uploads (file uploads persist across pod restarts)
- FileStorage__UploadPath env var set to /app/Uploads
- Replicas reduced to 1 (ReadWriteOnce PVC)
- Pipelines now: create namespace → apply manifests → restart/update
2026-02-23 20:11:56 +03:30
masoodafar-web 8cdb197ee7 chore: prevent appsettings.Production.json from merging — keep ours strategy
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m15s
2026-02-23 00:01:56 +03:30
masoodafar-web 2ca3fd29f9 feat: admin force activation — skip order/wallet checks for BackOffice
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m12s
- Proto: add force_activation field to ActivateClubMembershipRequest
- Command: add ForceActivation property
- Handler: wrap steps 2-5 in ForceActivation guard
  - Skip PackagePurchaseMethod check (set to DirectPurchase if None)
  - Skip wallet balance check
  - Skip Magic wallet mode check
  - Skip UserOrder/Transaction validation
- Proto NuGet bumped to 0.0.184
2026-02-22 23:51:39 +03:30
209 changed files with 15064 additions and 2696 deletions
+102
View File
@@ -0,0 +1,102 @@
name: Build and Deploy to Kubernetes
on:
push:
branches:
- kub-stage
env:
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/cms
K8S_SERVER: 194.5.195.53
jobs:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: 194.5.195.53:32082/docker-sshpass:latest
options: --privileged
steps:
- name: Start Docker daemon
run: |
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
}
DAEMON
echo "🚀 Starting Docker daemon..."
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
# Wait up to 3 minutes for Docker to be ready
for i in $(seq 1 90); do
if docker info >/dev/null 2>&1; then
echo "✅ Docker daemon is ready (attempt $i)"
docker version
break
else
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
sleep 2
fi
done
# Final check
if ! docker info >/dev/null 2>&1; then
echo "❌ Docker daemon failed to start after 3 minutes"
exit 1
fi
- name: Checkout code
run: |
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/CMS.git .
- name: Login to Docker registries
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
- name: Publish Protobuf packages
run: |
echo "📦 Publishing Protobuf packages..."
docker run --rm --network host -v $(pwd):/src -w /src \
194.5.195.53:32082/dotnet/sdk:9.0 sh -c '
for proj in $(find . -name "*Protobuf*.csproj" -type f); do
echo "📦 $proj"
dotnet restore "$proj" --configfile src/NuGet.config
dotnet build "$proj" -c Release --no-restore
dotnet pack "$proj" -c Release --no-build -o "$(dirname $proj)/nupkg"
for nupkg in $(dirname $proj)/nupkg/*.nupkg; do
[ -f "$nupkg" ] && dotnet nuget push "$nupkg" \
--source "http://194.5.195.53:32081/repository/foursat-nuget-hosted/index.json" \
--api-key "admin:87zH26nbqT" \
--skip-duplicate --allow-insecure-connections || true
done
done
'
echo "✅ Protobuf packages done!"
- name: Build Docker Image
run: |
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest .
- name: Push to Registry
run: |
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Deploy to Kubernetes
run: |
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
# Copy K8s manifests to server
sshpass -e scp -o StrictHostKeyChecking=no k8s/staging/cms-config.yaml root@${{ env.K8S_SERVER }}:/tmp/cms-config.yaml
sshpass -e scp -o StrictHostKeyChecking=no k8s/staging/cms-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/cms-deployment.yaml
# Apply config (Secret) first, then deployment
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
kubectl apply -f /tmp/cms-config.yaml &&
kubectl apply -f /tmp/cms-deployment.yaml &&
kubectl rollout restart deployment/cms &&
kubectl rollout status deployment/cms --timeout=180s &&
rm -f /tmp/cms-config.yaml /tmp/cms-deployment.yaml
"
echo "✅ Deployed!"
+4 -4
View File
@@ -4,7 +4,7 @@ apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: cms-uploads-pvc name: cms-uploads-pvc
namespace: foursat namespace: default
labels: labels:
app: cms app: cms
spec: spec:
@@ -20,7 +20,7 @@ apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: cms name: cms
namespace: foursat namespace: default
labels: labels:
app: cms app: cms
spec: spec:
@@ -89,7 +89,7 @@ apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: cms-svc name: cms-svc
namespace: foursat namespace: default
spec: spec:
selector: selector:
app: cms app: cms
@@ -105,7 +105,7 @@ apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: cms-ingress name: cms-ingress
namespace: foursat namespace: default
annotations: annotations:
kubernetes.io/ingress.class: "nginx" kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-prod" cert-manager.io/cluster-issuer: "letsencrypt-prod"
@@ -135,13 +135,25 @@ public class AcceptClubMembershipContractCommandHandler
}; };
await _context.UserContracts.AddAsync(userContract, cancellationToken); await _context.UserContracts.AddAsync(userContract, cancellationToken);
// 6. دریافت مقادیر از SystemConstants (استاتیک) // 6. بارگذاری پکیج از آخرین خرید کاربر یا پکیج پایه
long giftValue = SystemConstants.ClubMembershipGiftValue; var latestPurchase = await _context.UserPackagePurchases
long activationFeeValue = SystemConstants.ClubActivationFee; .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( _logger.LogInformation(
"Using Club.MembershipGiftValue: {GiftValue}, Club.ActivationFee: {ActivationFee}", "Using Package {PackageId} ({PackageName}): GiftValue={GiftValue}, ActivationFee={ActivationFee}",
giftValue, activationFeeValue package.Id, package.Title, giftValue, activationFeeValue
); );
// 7. فعال‌سازی باشگاه مشتریان // 7. فعال‌سازی باشگاه مشتریان
@@ -156,6 +168,10 @@ public class AcceptClubMembershipContractCommandHandler
UserId = user.Id, UserId = user.Id,
IsActive = true, IsActive = true,
ActivatedAt = activationDate, ActivatedAt = activationDate,
FirstActivationDate = activationDate,
FirstPackageId = package.Id,
LastActivationDate = activationDate,
LastPackageId = package.Id,
InitialContribution = activationFeeValue, InitialContribution = activationFeeValue,
GiftValue = giftValue, GiftValue = giftValue,
TotalEarned = 0, TotalEarned = 0,
@@ -175,6 +191,8 @@ public class AcceptClubMembershipContractCommandHandler
clubMembership = user.ClubMembership!; clubMembership = user.ClubMembership!;
clubMembership.IsActive = true; clubMembership.IsActive = true;
clubMembership.ActivatedAt = activationDate; clubMembership.ActivatedAt = activationDate;
clubMembership.LastActivationDate = activationDate;
clubMembership.LastPackageId = package.Id;
clubMembership.PurchaseMethod = user.PackagePurchaseMethod; clubMembership.PurchaseMethod = user.PackagePurchaseMethod;
_context.ClubMemberships.Update(clubMembership); _context.ClubMemberships.Update(clubMembership);
@@ -206,13 +224,14 @@ public class AcceptClubMembershipContractCommandHandler
// 9. اضافه کردن مبلغ به Pool هفته جاری // 9. اضافه کردن مبلغ به Pool هفته جاری
var currentWeekDefinitionId = GetCurrentWeekDefinitionId(); var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
var weeklyPool = await _context.WeeklyCommissionPools var weeklyPool = await _context.WeeklyCommissionPools
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId, cancellationToken); .FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
if (weeklyPool == null) if (weeklyPool == null)
{ {
weeklyPool = new WeeklyCommissionPool weeklyPool = new WeeklyCommissionPool
{ {
WeekDefinitionId = currentWeekDefinitionId, WeekDefinitionId = currentWeekDefinitionId,
PackageId = package.Id,
TotalPoolAmount = activationFeeValue, TotalPoolAmount = activationFeeValue,
TotalBalances = 0, TotalBalances = 0,
ValuePerBalance = 0, ValuePerBalance = 0,
@@ -243,36 +262,8 @@ public class AcceptClubMembershipContractCommandHandler
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
// 10. اعطای ویژگی‌های باشگاه برای کاربر (فقط برای عضویت جدید) // 10. اعمال ویژگی‌های پکیج — DIFF/تفاضل (Q20)
if (isNewMembership) await ApplyFeatureDiffAsync(user.Id, clubMembership.Id, package.Id, isNewMembership, activationDate, cancellationToken);
{
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
var clubFeatures = await _context.ClubFeatures
.Where(f => !f.IsDeleted && featureIds.Contains(f.Id))
.ToListAsync(cancellationToken);
if (clubFeatures.Any())
{
var userClubFeatures = clubFeatures.Select(feature => new UserClubFeature
{
UserId = user.Id,
ClubMembershipId = clubMembership.Id,
ClubFeatureId = feature.Id,
GrantedAt = activationDate,
IsActive = true,
Notes = "اعطا شده هنگام امضای قرارداد"
}).ToList();
_context.UserClubFeatures.AddRange(userClubFeatures);
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Granted {Count} club features to UserId {UserId}",
clubFeatures.Count,
user.Id
);
}
}
_logger.LogInformation( _logger.LogInformation(
"Club membership contract accepted and activated for UserId: {UserId}, ContractId: {ContractId}, MembershipId: {MembershipId}", "Club membership contract accepted and activated for UserId: {UserId}, ContractId: {ContractId}, MembershipId: {MembershipId}",
@@ -302,6 +293,61 @@ public class AcceptClubMembershipContractCommandHandler
return week.Id; return week.Id;
} }
private async Task ApplyFeatureDiffAsync(
long userId, long membershipId, long packageId,
bool isNewMembership, DateTime activationDate,
CancellationToken cancellationToken)
{
var newFeatureIds = await _context.PackageFeatures
.Where(pf => pf.PackageId == packageId && pf.IsIncluded && !pf.IsDeleted)
.Select(pf => pf.ClubFeatureId)
.ToListAsync(cancellationToken);
if (!newFeatureIds.Any())
{
_logger.LogWarning("No PackageFeatures for PackageId {PackageId}", packageId);
return;
}
var currentFeatures = await _context.UserClubFeatures
.Where(ucf => ucf.UserId == userId && ucf.IsActive)
.ToListAsync(cancellationToken);
var currentFeatureIds = currentFeatures.Select(f => f.ClubFeatureId).ToHashSet();
var toAdd = newFeatureIds.Where(id => !currentFeatureIds.Contains(id)).ToList();
var toRemove = currentFeatures.Where(f => !newFeatureIds.Contains(f.ClubFeatureId)).ToList();
foreach (var feature in toRemove)
{
feature.IsActive = false;
feature.Notes = $"حذف شده بابت تغییر پکیج (PackageId: {packageId})";
}
if (toAdd.Any())
{
var newUserFeatures = toAdd.Select(featureId => new UserClubFeature
{
UserId = userId,
ClubMembershipId = membershipId,
ClubFeatureId = featureId,
GrantedAt = activationDate,
IsActive = true,
Notes = isNewMembership
? "اعطا شده هنگام امضای قرارداد"
: $"اضافه شده بابت خرید مجدد پکیج (PackageId: {packageId})"
}).ToList();
_context.UserClubFeatures.AddRange(newUserFeatures);
}
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Feature DIFF applied for UserId={UserId}: Added={Added}, Removed={Removed}, Kept={Kept}",
userId, toAdd.Count, toRemove.Count,
currentFeatureIds.Count - toRemove.Count);
}
private async Task<(bool Success, string Message)> VerifyOtpAsync( private async Task<(bool Success, string Message)> VerifyOtpAsync(
string mobile, string mobile,
string code, string code,
@@ -12,4 +12,9 @@ public record ActivateClubMembershipCommand : IRequest<bool>
/// شناسه کاربر /// شناسه کاربر
/// </summary> /// </summary>
public long UserId { get; init; } public long UserId { get; init; }
/// <summary>
/// فعال‌سازی اجباری توسط ادمین — بدون بررسی سفارش و کیف‌پول
/// </summary>
public bool ForceActivation { get; init; }
} }
@@ -54,15 +54,21 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
throw new NotFoundException(nameof(User), request.UserId); throw new NotFoundException(nameof(User), request.UserId);
} }
// متغیر پکیج — در هر دو مسیر (عادی و Force) مقداردهی می‌شود
Package package;
// 2-5: بررسی‌های مالی — در حالت ForceActivation (ادمین) رد می‌شود
if (!request.ForceActivation)
{
// 2. بررسی اینکه پکیج خریده باشد // 2. بررسی اینکه پکیج خریده باشد
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None) if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
{ {
_logger.LogWarning( _logger.LogWarning(
"User {UserId} has not purchased golden package yet", "User {UserId} has not purchased any package yet",
request.UserId request.UserId
); );
throw new BadRequestException( throw new BadRequestException(
"برای فعالسازی باشگاه مشتریان ابتدا باید پکیج طلایی خریداری کنید" "برای فعالسازی باشگاه مشتریان ابتدا باید یک پکیج خریداری کنید"
); );
} }
@@ -76,17 +82,8 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
throw new NotFoundException("کیف پول کاربر یافت نشد"); throw new NotFoundException("کیف پول کاربر یافت نشد");
} }
if (wallet.Balance < SystemConstants.BasePackageAmount) // NOTE: balance check uses package.Price — loaded after finding order (step 4)
{ // Moved to after package loading
_logger.LogWarning(
"User {UserId} has insufficient balance: {Balance}",
request.UserId,
wallet.Balance
);
throw new BadRequestException(
$"برای فعالسازی باشگاه مشتریان باید حداقل {SystemConstants.BasePackageAmount:N0} ریال موجودی اصلی داشته باشید"
);
}
// 3.5. بررسی وضعیت کیف‌پول جادویی — اگر در حالت Magic است، فعالسازی مجاز نیست // 3.5. بررسی وضعیت کیف‌پول جادویی — اگر در حالت Magic است، فعالسازی مجاز نیست
if (wallet.WalletMode == WalletMode.Magic) if (wallet.WalletMode == WalletMode.Magic)
@@ -100,43 +97,44 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
); );
} }
// 4. پیدا کردن UserOrder با PackageId // 4. پیدا کردن آخرین خرید موفق پکیج
var packageOrder = await _context.UserOrders var packagePurchase = await _context.UserPackagePurchases
.Include(o => o.Transaction) .Include(p => p.Transaction)
.Where(o => .Where(p =>
o.UserId == user.Id && p.UserId == user.Id &&
o.PackageId != null && p.Transaction != null &&
o.PaymentStatus == PaymentStatus.Success) p.Transaction.PaymentStatus == PaymentStatus.Success)
.OrderByDescending(o => o.Created) .OrderByDescending(p => p.Created)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
if (packageOrder == null) if (packagePurchase == null)
{ {
_logger.LogWarning( _logger.LogWarning(
"No successful package order found for UserId: {UserId}", "No successful package purchase found for UserId: {UserId}",
request.UserId request.UserId
); );
throw new NotFoundException("سفارش پکیج طلایی یافت نشد"); throw new NotFoundException("سفارش پکیج یافت نشد");
} }
// 5. بررسی Transaction // 5. بررسی Transaction
if (packageOrder.Transaction == null) if (packagePurchase.Transaction == null)
{ {
_logger.LogError( _logger.LogError(
"Transaction not found for OrderId: {OrderId}", "Transaction not found for PurchaseId: {PurchaseId}",
packageOrder.Id packagePurchase.Id
); );
throw new NotFoundException("تراکنش مربوط به سفارش یافت نشد"); throw new NotFoundException("تراکنش مربوط به سفارش یافت نشد");
} }
var transaction = packageOrder.Transaction; var transaction = packagePurchase.Transaction;
if (transaction.Type != TransactionType.DepositIpg && if (transaction.Type != TransactionType.Buy &&
transaction.Type != TransactionType.DepositIpg &&
transaction.Type != TransactionType.DepositExternal1) transaction.Type != TransactionType.DepositExternal1)
{ {
_logger.LogWarning( _logger.LogWarning(
"Invalid transaction type for OrderId {OrderId}: {Type}", "Invalid transaction type for PurchaseId {PurchaseId}: {Type}",
packageOrder.Id, packagePurchase.Id,
transaction.Type transaction.Type
); );
throw new BadRequestException( throw new BadRequestException(
@@ -144,17 +142,51 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
); );
} }
// 5.5. بارگذاری پکیج از خرید
package = await _context.Packages
.FirstOrDefaultAsync(p => p.Id == packagePurchase.PackageId && !p.IsDeleted, cancellationToken)
?? throw new NotFoundException("پکیج یافت نشد");
// بررسی موجودی با مبلغ پکیج واقعی
if (wallet.Balance < package.Price)
{
throw new BadRequestException(
$"برای فعالسازی باشگاه مشتریان باید حداقل {package.Price:N0} تومان موجودی اصلی داشته باشید"
);
}
}
else
{
_logger.LogInformation(
"Force activation by admin for UserId: {UserId} — skipping order/wallet validation",
request.UserId
);
// در فعال‌سازی اجباری، اگر PackagePurchaseMethod تنظیم نشده، مقدار DirectPurchase بگذار
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
{
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
_context.Users.Update(user);
await _context.SaveChangesAsync(cancellationToken);
}
// بارگذاری پکیج پایه برای فعال‌سازی اجباری
package = await _context.Packages
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
?? throw new NotFoundException("پکیج پایه یافت نشد");
}
// 6. بررسی عضویت فعلی // 6. بررسی عضویت فعلی
var existingMembership = await _context.ClubMemberships var existingMembership = await _context.ClubMemberships
.FirstOrDefaultAsync(c => c.UserId == user.Id, cancellationToken); .FirstOrDefaultAsync(c => c.UserId == user.Id, cancellationToken);
// 6.1. دریافت مبلغ هدیه و هزینه فعالسازی از SystemConstants // 6.1. دریافت مبلغ هدیه و هزینه فعالسازی از پکیج
long giftValue = SystemConstants.ClubMembershipGiftValue; long giftValue = package.ActivationFee;
long activationFeeValue = SystemConstants.ClubActivationFee; long activationFeeValue = package.ActivationFee;
_logger.LogInformation( _logger.LogInformation(
"Using Club.MembershipGiftValue: {GiftValue}, Club.ActivationFee: {ActivationFee}", "Using Package {PackageId} ({PackageName}): GiftValue={GiftValue}, ActivationFee={ActivationFee}, Price={Price}",
giftValue, activationFeeValue package.Id, package.Title, giftValue, activationFeeValue, package.Price
); );
ClubMembership entity; ClubMembership entity;
@@ -163,14 +195,18 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
if (isNewMembership) if (isNewMembership)
{ {
// ایجاد عضویت جدید // ایجاد عضویت جدید — IsActive = false تا زمان امضای قرارداد باشگاه
entity = new ClubMembership entity = new ClubMembership
{ {
UserId = user.Id, UserId = user.Id,
IsActive = true, IsActive = false,
ActivatedAt = activationDate, ActivatedAt = activationDate,
InitialContribution =activationFeeValue, FirstActivationDate = activationDate,
GiftValue = giftValue, // مقدار از تنظیمات FirstPackageId = package.Id,
LastActivationDate = activationDate,
LastPackageId = package.Id,
InitialContribution = activationFeeValue,
GiftValue = giftValue,
TotalEarned = 0, TotalEarned = 0,
PurchaseMethod = user.PackagePurchaseMethod PurchaseMethod = user.PackagePurchaseMethod
}; };
@@ -186,25 +222,32 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
} }
else else
{ {
if (existingMembership.IsActive) // بررسی آیا چرخه فعال دارد — اگر بله، واقعاً فعال است و نیازی به کار نیست
var hasCurrentCycle = await _context.ClubMembershipCycles
.AnyAsync(c => c.UserId == user.Id && c.IsCurrentCycle, cancellationToken);
if (existingMembership.IsActive && hasCurrentCycle)
{ {
_logger.LogInformation( _logger.LogInformation(
"User {UserId} is already an active club member", "User {UserId} is already an active club member with a running cycle",
user.Id user.Id
); );
return true; return true;
} }
// فعال‌سازی مجدد ActivatedAt حفظ میشه (overwrite نمیشه) // فعال‌سازی مجدد یا خرید مجدد (Q19/Q20) — FirstActivation حفظ میشه (Q21), Last بروزرسانی میشه
// مقدار IsActive قبلی حفظ میشه — اگر قبلاً فعال بوده، فعال بمونه
entity = existingMembership; entity = existingMembership;
entity.IsActive = true; entity.LastActivationDate = activationDate;
entity.LastPackageId = package.Id;
entity.PurchaseMethod = user.PackagePurchaseMethod; entity.PurchaseMethod = user.PackagePurchaseMethod;
_context.ClubMemberships.Update(entity); _context.ClubMemberships.Update(entity);
_logger.LogInformation( _logger.LogInformation(
"Reactivated club membership for UserId {UserId}", "Re-activating club membership for UserId {UserId} (re-purchase scenario, IsActive was {WasActive})",
user.Id user.Id,
existingMembership.IsActive
); );
} }
@@ -231,7 +274,8 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
CycleNumber = maxCycleNumber + 1, CycleNumber = maxCycleNumber + 1,
PackagePurchasedAt = activationDate, PackagePurchasedAt = activationDate,
PurchaseMethod = user.PackagePurchaseMethod, PurchaseMethod = user.PackagePurchaseMethod,
PackageAmount = SystemConstants.BasePackageAmount, PackageAmount = package.Price,
PackageId = package.Id,
IsCurrentCycle = true IsCurrentCycle = true
}; };
@@ -265,15 +309,16 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
// ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری // ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری
var currentWeekDefinitionId = GetCurrentWeekDefinitionId(); var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
var weeklyPool = await _context.WeeklyCommissionPools var weeklyPool = await _context.WeeklyCommissionPools
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId, cancellationToken); .FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
if (weeklyPool == null) if (weeklyPool == null)
{ {
// ایجاد Pool جدید برای این هفته // ایجاد Pool جدید برای این هفته × این پکیج
weeklyPool = new WeeklyCommissionPool weeklyPool = new WeeklyCommissionPool
{ {
WeekDefinitionId = currentWeekDefinitionId, WeekDefinitionId = currentWeekDefinitionId,
TotalPoolAmount = activationFeeValue, // مبلغ هدیه به Pool اضافه میشه PackageId = package.Id,
TotalPoolAmount = activationFeeValue,
TotalBalances = 0, // در CalculateWeeklyBalances محاسبه میشه TotalBalances = 0, // در CalculateWeeklyBalances محاسبه میشه
ValuePerBalance = 0, // در CalculateWeeklyCommissionPool محاسبه میشه ValuePerBalance = 0, // در CalculateWeeklyCommissionPool محاسبه میشه
IsCalculated = false, IsCalculated = false,
@@ -304,41 +349,8 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
// 9. اضافه کردن ویژگی‌های باشگاه برای کاربر (فقط برای عضویت جدید) // 9. اعمال ویژگی‌های پکیج — DIFF/تفاضل (Q20)
if (isNewMembership) await ApplyFeatureDiffAsync(user.Id, entity.Id, package.Id, isNewMembership, activationDate, cancellationToken);
{
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
var clubFeatures = await _context.ClubFeatures
.Where(f => !f.IsDeleted && featureIds.Contains(f.Id))
.ToListAsync(cancellationToken);
if (clubFeatures.Any())
{
var userClubFeatures = clubFeatures.Select(feature => new UserClubFeature
{
UserId = user.Id,
ClubMembershipId = entity.Id,
ClubFeatureId = feature.Id,
GrantedAt = activationDate,
IsActive = true,
Notes = "اعطا شده به‌طور خودکار هنگام فعالسازی"
}).ToList(); _context.UserClubFeatures.AddRange(userClubFeatures);
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Granted {Count} club features to UserId {UserId}",
clubFeatures.Count,
user.Id
);
}
else
{
_logger.LogWarning(
"No club features found to grant to UserId {UserId}",
user.Id
);
}
}
_logger.LogInformation( _logger.LogInformation(
"Club membership activated successfully. UserId: {UserId}, MembershipId: {MembershipId}", "Club membership activated successfully. UserId: {UserId}, MembershipId: {MembershipId}",
@@ -359,6 +371,74 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
} }
} }
/// <summary>
/// اعمال DIFF فیچرها بر اساس پکیج (Q20)
/// عضویت جدید: همه فیچرهای پکیج اضافه میشه
/// خرید مجدد: تفاضل فیچرهای فعلی و پکیج جدید
/// </summary>
private async Task ApplyFeatureDiffAsync(
long userId, long membershipId, long packageId,
bool isNewMembership, DateTime activationDate,
CancellationToken cancellationToken)
{
// فیچرهای پکیج جدید
var newFeatureIds = await _context.PackageFeatures
.Where(pf => pf.PackageId == packageId && pf.IsIncluded && !pf.IsDeleted)
.Select(pf => pf.ClubFeatureId)
.ToListAsync(cancellationToken);
if (!newFeatureIds.Any())
{
_logger.LogWarning(
"No PackageFeatures found for PackageId {PackageId} — skipping feature assignment for UserId {UserId}",
packageId, userId);
return;
}
// فیچرهای فعال فعلی کاربر
var currentFeatures = await _context.UserClubFeatures
.Where(ucf => ucf.UserId == userId && ucf.IsActive)
.ToListAsync(cancellationToken);
var currentFeatureIds = currentFeatures.Select(f => f.ClubFeatureId).ToHashSet();
// DIFF
var toAdd = newFeatureIds.Where(id => !currentFeatureIds.Contains(id)).ToList();
var toRemove = currentFeatures.Where(f => !newFeatureIds.Contains(f.ClubFeatureId)).ToList();
// حذف فیچرهای قدیمی که پکیج جدید ندارد
foreach (var feature in toRemove)
{
feature.IsActive = false;
feature.Notes = $"حذف شده بابت تغییر پکیج (PackageId: {packageId})";
}
// اضافه فیچرهای جدید
if (toAdd.Any())
{
var newUserFeatures = toAdd.Select(featureId => new UserClubFeature
{
UserId = userId,
ClubMembershipId = membershipId,
ClubFeatureId = featureId,
GrantedAt = activationDate,
IsActive = true,
Notes = isNewMembership
? "اعطا شده هنگام فعالسازی اولیه"
: $"اضافه شده بابت خرید مجدد پکیج (PackageId: {packageId})"
}).ToList();
_context.UserClubFeatures.AddRange(newUserFeatures);
}
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Feature DIFF applied for UserId={UserId}: Added={Added}, Removed={Removed}, Kept={Kept}",
userId, toAdd.Count, toRemove.Count,
currentFeatureIds.Count - toRemove.Count);
}
/// <summary> /// <summary>
/// دریافت شناسه تعریف هفته جاری /// دریافت شناسه تعریف هفته جاری
/// </summary> /// </summary>
@@ -82,11 +82,14 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
var balancesList = new List<NetworkWeeklyBalance>(); var balancesList = new List<NetworkWeeklyBalance>();
var calculatedAt = DateTime.Now; var calculatedAt = DateTime.Now;
// استفاده از SystemConstants (استاتیک - بدون کوئری به دیتابیس) // بارگذاری پکیج پایه برای خواندن سقف‌ها
// سقف تعادل هفتگی برای هر دست (نه کل) - 300 برای چپ + 300 برای راست = حداکثر 600 تعادل var package = await _context.Packages
var maxBalancesPerLeg = SystemConstants.CommissionMaxWeeklyBalancesPerLeg; .FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
// حداکثر عمق شبکه برای شمارش اعضا (15 لول) ?? throw new InvalidOperationException("پکیج پایه یافت نشد");
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
// خواندن سقف‌ها از پکیج (قبلاً از SystemConstants بود)
var maxBalancesPerLeg = package.MaxBalancesPerLeg;
var maxNetworkLevel = package.MaxNetworkLevel;
foreach (var user in usersInNetwork.OrderBy(o=>o.Id)) foreach (var user in usersInNetwork.OrderBy(o=>o.Id))
{ {
@@ -140,6 +143,7 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
{ {
UserId = user.Id, UserId = user.Id,
WeekDefinitionId = request.WeekDefinitionId, WeekDefinitionId = request.WeekDefinitionId,
PackageId = package.Id,
// اطلاعات جدید // اطلاعات جدید
LeftLegNewMembers = leftNewMembers, LeftLegNewMembers = leftNewMembers,
@@ -192,7 +192,7 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
.ToDictionaryAsync(w => w.UserId, cancellationToken); .ToDictionaryAsync(w => w.UserId, cancellationToken);
var newWallets = new List<UserWallet>(); var newWallets = new List<UserWallet>();
var walletLogs = new List<UserWalletChangeLog>(); var walletLogs = new List<UserWalletHistory>();
foreach (var payout in payouts) foreach (var payout in payouts)
{ {
@@ -241,7 +241,7 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
wallet.NetworkBalance += payout.TotalAmount; wallet.NetworkBalance += payout.TotalAmount;
// ایجاد لاگ تغییر کیف پول // ایجاد لاگ تغییر کیف پول
var walletLog = new UserWalletChangeLog var walletLog = new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = wallet.Balance, CurrentBalance = wallet.Balance,
@@ -258,7 +258,7 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
} }
// ذخیره تغییرات کیف پول و لاگ‌ها // ذخیره تغییرات کیف پول و لاگ‌ها
await _context.UserWalletChangeLogs.AddRangeAsync(walletLogs, cancellationToken); await _context.UserWalletHistories.AddRangeAsync(walletLogs, cancellationToken);
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
} }
@@ -271,7 +271,7 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// پیدا کردن لاگ‌های کیف پول مرتبط با پرداخت‌های قبلی // پیدا کردن لاگ‌های کیف پول مرتبط با پرداخت‌های قبلی
var oldWalletLogs = await _context.UserWalletChangeLogs var oldWalletLogs = await _context.UserWalletHistories
.Where(l => l.RefrenceId.HasValue && oldPayoutIds.Contains(l.RefrenceId.Value)) .Where(l => l.RefrenceId.HasValue && oldPayoutIds.Contains(l.RefrenceId.Value))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -300,7 +300,7 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
} }
// حذف لاگ‌های قبلی // حذف لاگ‌های قبلی
_context.UserWalletChangeLogs.RemoveRange(oldWalletLogs); _context.UserWalletHistories.RemoveRange(oldWalletLogs);
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
} }
@@ -49,8 +49,11 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
await _context.SaveChangesAsync(cancellationToken); 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 var allWeeklyBalances = await _context.NetworkWeeklyBalances
@@ -113,6 +116,7 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
UserId = userId, UserId = userId,
WeekDefinitionId = request.WeekDefinitionId, WeekDefinitionId = request.WeekDefinitionId,
WeeklyPoolId = pool.Id, WeeklyPoolId = pool.Id,
PackageId = package.Id,
BalancesEarned = totalBalancesWithSubordinates, // ⭐ شامل زیرمجموعه BalancesEarned = totalBalancesWithSubordinates, // ⭐ شامل زیرمجموعه
ValuePerBalance = pool.ValuePerBalance, ValuePerBalance = pool.ValuePerBalance,
TotalAmount = totalAmount, TotalAmount = totalAmount,
@@ -18,6 +18,11 @@ public record GetMyCommissionPayoutsQuery : IRequest<GetMyCommissionPayoutsRespo
/// </summary> /// </summary>
public long? WeekDefinitionId { get; init; } public long? WeekDefinitionId { get; init; }
/// <summary>
/// فیلتر بر اساس پکیج (اختیاری)
/// </summary>
public long? PackageId { get; init; }
/// <summary> /// <summary>
/// Pagination /// Pagination
/// </summary> /// </summary>
@@ -28,6 +28,7 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissio
var query = _context.UserCommissionPayouts var query = _context.UserCommissionPayouts
.Include(x => x.WeekDefinition) .Include(x => x.WeekDefinition)
.Include(x => x.Package)
.Where(x => x.UserId == userId) .Where(x => x.UserId == userId)
.AsNoTracking() .AsNoTracking()
.AsQueryable(); .AsQueryable();
@@ -43,6 +44,12 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissio
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId.Value); 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); query = query.OrderByDescending(x => x.Created);
@@ -60,7 +67,9 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissio
AmountFormatted = x.TotalAmount.ToString("N0") + " تومان", AmountFormatted = x.TotalAmount.ToString("N0") + " تومان",
Status = x.Status, Status = x.Status,
CalculatedDate = x.PaidAt ?? (DateTime?)x.Created, CalculatedDate = x.PaidAt ?? (DateTime?)x.Created,
DatePersian = "" DatePersian = "",
PackageId = x.PackageId,
PackageTitle = x.Package != null ? x.Package.Title : ""
}) })
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -20,4 +20,8 @@ public class GetMyCommissionPayoutsResponseModel
public CommissionPayoutStatus Status { get; set; } public CommissionPayoutStatus Status { get; set; }
public DateTime? CalculatedDate { get; set; } public DateTime? CalculatedDate { get; set; }
public string DatePersian { get; set; } = string.Empty; 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> /// </summary>
public bool OnlyActive { get; init; } public bool OnlyActive { get; init; }
/// <summary>
/// فیلتر بر اساس پکیج (اختیاری)
/// </summary>
public long? PackageId { get; init; }
/// <summary> /// <summary>
/// Pagination /// Pagination
/// </summary> /// </summary>
@@ -38,6 +38,7 @@ public class GetMyWeeklyBalancesQueryHandler : IRequestHandler<GetMyWeeklyBalanc
{ {
UserId = userId, UserId = userId,
WeekDefinitionId = request.WeekDefinitionId, WeekDefinitionId = request.WeekDefinitionId,
PackageId = request.PackageId,
OnlyActive = request.OnlyActive, OnlyActive = request.OnlyActive,
PaginationState = request.PaginationState PaginationState = request.PaginationState
}; };
@@ -20,6 +20,11 @@ public record GetUserCommissionPayoutsQuery : IRequest<GetUserCommissionPayoutsR
/// </summary> /// </summary>
public long? WeekDefinitionId { get; init; } public long? WeekDefinitionId { get; init; }
/// <summary>
/// فیلتر بر اساس پکیج (اختیاری)
/// </summary>
public long? PackageId { get; init; }
/// <summary> /// <summary>
/// مرتب‌سازی /// مرتب‌سازی
/// </summary> /// </summary>
@@ -18,6 +18,7 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
var query = _context.UserCommissionPayouts var query = _context.UserCommissionPayouts
.Include(x => x.WeekDefinition) .Include(x => x.WeekDefinition)
.Include(x => x.User) .Include(x => x.User)
.Include(x => x.Package)
.AsNoTracking() .AsNoTracking()
.AsQueryable(); .AsQueryable();
@@ -41,6 +42,12 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId); 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"); query = query.ApplyOrder(sortBy: request.SortBy ?? "Created");
var meta = await query.GetMetaData(request.PaginationState, cancellationToken); var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
@@ -64,7 +71,9 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
WithdrawalMethod = x.WithdrawalMethod, WithdrawalMethod = x.WithdrawalMethod,
IbanNumber = x.IbanNumber, IbanNumber = x.IbanNumber,
WithdrawnAt = x.WithdrawnAt, WithdrawnAt = x.WithdrawnAt,
Created = x.Created Created = x.Created,
PackageId = x.PackageId,
PackageTitle = x.Package != null ? x.Package.Title : ""
}) })
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -24,4 +24,8 @@ public class GetUserCommissionPayoutsResponseModel
public string? IbanNumber { get; set; } public string? IbanNumber { get; set; }
public DateTime? WithdrawnAt { get; set; } public DateTime? WithdrawnAt { get; set; }
public DateTimeOffset Created { 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> /// </summary>
public long? WeekDefinitionId { get; init; } public long? WeekDefinitionId { get; init; }
/// <summary>
/// فیلتر بر اساس پکیج (اختیاری)
/// </summary>
public long? PackageId { get; init; }
/// <summary> /// <summary>
/// فقط موارد Expired نشده؟ /// فقط موارد Expired نشده؟
@@ -18,6 +18,7 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
var query = _context.NetworkWeeklyBalances var query = _context.NetworkWeeklyBalances
.Include(x => x.WeekDefinition) .Include(x => x.WeekDefinition)
.Include(x => x.User) .Include(x => x.User)
.Include(x => x.Package)
.AsNoTracking() .AsNoTracking()
.AsQueryable(); .AsQueryable();
@@ -43,6 +44,12 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
query = query.Where(x => !x.IsExpired); query = query.Where(x => !x.IsExpired);
} }
// فیلتر بر اساس پکیج
if (request.PackageId.HasValue && request.PackageId.Value > 0)
{
query = query.Where(x => x.PackageId == request.PackageId.Value);
}
// مرتب‌سازی بر اساس WeekDefinitionId (نزولی = جدیدترین اول) // مرتب‌سازی بر اساس WeekDefinitionId (نزولی = جدیدترین اول)
query = query.ApplyOrder(sortBy: request.SortBy ?? "-WeekDefinitionId"); query = query.ApplyOrder(sortBy: request.SortBy ?? "-WeekDefinitionId");
@@ -67,7 +74,9 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
WeeklyPoolContribution = x.WeeklyPoolContribution, WeeklyPoolContribution = x.WeeklyPoolContribution,
CalculatedAt = x.CalculatedAt, CalculatedAt = x.CalculatedAt,
IsExpired = x.IsExpired, IsExpired = x.IsExpired,
Created = x.Created Created = x.Created,
PackageId = x.PackageId,
PackageTitle = x.Package != null ? x.Package.Title : ""
}) })
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -29,4 +29,8 @@ public class GetUserWeeklyBalancesResponseModel
public DateTime? CalculatedAt { get; set; } public DateTime? CalculatedAt { get; set; }
public bool IsExpired { get; set; } public bool IsExpired { get; set; }
public DateTimeOffset Created { get; set; } public DateTimeOffset Created { get; set; }
// Package info
public long PackageId { get; set; }
public string PackageTitle { get; set; } = string.Empty;
} }
@@ -51,6 +51,15 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
.PaginatedListAsync(paginationState: request.PaginationState) .PaginatedListAsync(paginationState: request.PaginationState)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
// دریافت PackageName از آخرین خرید پکیج کاربران
var userIds = models.Select(x => x.UserId).Distinct().ToList();
var userPackageNames = await _context.UserPackagePurchases
.Where(p => userIds.Contains(p.UserId))
.Include(p => p.Package)
.GroupBy(p => p.UserId)
.Select(g => new { UserId = g.Key, PackageName = g.OrderByDescending(x => x.PurchasedAt).First().Package.Title })
.ToDictionaryAsync(x => x.UserId, x => x.PackageName, cancellationToken);
var result = models.Select(x => new WithdrawalRequestModel var result = models.Select(x => new WithdrawalRequestModel
{ {
Id = x.Id, Id = x.Id,
@@ -69,7 +78,8 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
BankReferenceId = x.BankReferenceId, BankReferenceId = x.BankReferenceId,
BankTrackingCode = x.BankTrackingCode, BankTrackingCode = x.BankTrackingCode,
PaymentFailureReason = x.PaymentFailureReason, PaymentFailureReason = x.PaymentFailureReason,
Created = x.Created Created = x.Created,
PackageName = userPackageNames.GetValueOrDefault(x.UserId)
}).ToList(); }).ToList();
return new GetWithdrawalRequestsResponseDto return new GetWithdrawalRequestsResponseDto
@@ -25,4 +25,5 @@ public class WithdrawalRequestModel
public string? BankTrackingCode { get; set; } public string? BankTrackingCode { get; set; }
public string? PaymentFailureReason { get; set; } public string? PaymentFailureReason { get; set; }
public DateTime Created { get; set; } public DateTime Created { get; set; }
public string? PackageName { get; set; }
} }
@@ -35,6 +35,7 @@ public interface IUserNotificationService
long userId, long userId,
decimal amount, decimal amount,
int weekNumber, int weekNumber,
string? packageName = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -42,6 +43,7 @@ public interface IUserNotificationService
/// </summary> /// </summary>
Task SendClubActivationNotificationAsync( Task SendClubActivationNotificationAsync(
long userId, long userId,
string? packageName = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -50,5 +52,6 @@ public interface IUserNotificationService
Task SendPayoutErrorNotificationAsync( Task SendPayoutErrorNotificationAsync(
long userId, long userId,
string errorMessage, string errorMessage,
string? packageName = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
} }
@@ -32,13 +32,16 @@ public interface IApplicationDbContext
DbSet<OrderVAT> OrderVATs { get; } DbSet<OrderVAT> OrderVATs { get; }
DbSet<UserPackagePurchase> UserPackagePurchases { get; } DbSet<UserPackagePurchase> UserPackagePurchases { get; }
DbSet<UserWallet> UserWallets { get; } DbSet<UserWallet> UserWallets { get; }
DbSet<UserWalletChangeLog> UserWalletChangeLogs { get; } DbSet<UserWalletHistory> UserWalletHistories { get; }
DbSet<ManualPayment> ManualPayments { get; } DbSet<ManualPayment> ManualPayments { get; }
DbSet<PaymentTransaction> PaymentTransactions { get; } DbSet<PaymentTransaction> PaymentTransactions { get; }
DbSet<PublicMessage> PublicMessages { get; } DbSet<PublicMessage> PublicMessages { get; }
DbSet<ClubMembership> ClubMemberships { get; } DbSet<ClubMembership> ClubMemberships { get; }
DbSet<ClubMembershipHistory> ClubMembershipHistories { get; } DbSet<ClubMembershipHistory> ClubMembershipHistories { get; }
DbSet<ClubMembershipCycle> ClubMembershipCycles { get; } DbSet<ClubMembershipCycle> ClubMembershipCycles { get; }
DbSet<ClubMembershipCycleHistory> ClubMembershipCycleHistories { get; }
DbSet<PackageHistory> PackageHistories { get; }
DbSet<PackageFeature> PackageFeatures { get; }
DbSet<ClubFeature> ClubFeatures { get; } DbSet<ClubFeature> ClubFeatures { get; }
DbSet<UserClubFeature> UserClubFeatures { get; } DbSet<UserClubFeature> UserClubFeatures { get; }
DbSet<NetworkWeeklyBalance> NetworkWeeklyBalances { get; } DbSet<NetworkWeeklyBalance> NetworkWeeklyBalances { get; }
@@ -13,5 +13,10 @@ public interface IKavenegarService
/// <summary> /// <summary>
/// ارسال پیامک با قالب (VerifyLookup) /// ارسال پیامک با قالب (VerifyLookup)
/// </summary> /// </summary>
Task VerifyLookupAsync(string mobile, string token, string template = "Afrino"); Task VerifyLookupAsync(string mobile, string token, string template = "OTP");
/// <summary>
/// ارسال پیامک با قالب و توکن دوم (VerifyLookup) — مثلاً برای قراردادها
/// </summary>
Task VerifyLookupAsync(string mobile, string token, string token2, string template);
} }
@@ -41,8 +41,10 @@ public interface IPaymentGatewayService
decimal amountInToman, decimal amountInToman,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
// پیش‌فرض: درگاه‌هایی که Amount نمی‌خواهند، از overload بدون amount استفاده کنند // ⚠️ هشدار: این پیاده‌سازی پیش‌فرض مبلغ را نادیده می‌گیرد.
return VerifyPaymentAsync(refId, verificationToken, cancellationToken); // درگاه‌هایی مثل زرین‌پال باید حتماً این متد را override کنند.
throw new NotImplementedException(
"درگاه پرداخت باید متد VerifyPaymentAsync با مبلغ را پیاده‌سازی کند");
} }
/// <summary> /// <summary>
@@ -1,16 +0,0 @@
using CMSMicroservice.Application.UserWalletChangeLogCQ.Queries.GetAllUserWalletChangeLogByFilter;
using CMSMicroservice.Application.UserWalletChangeLogCQ.Queries.GetUserWalletChangeLog;
namespace CMSMicroservice.Application.Common.Mappings;
public class UserWalletChangeLogProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
config.NewConfig<UserWalletChangeLog,GetAllUserWalletChangeLogByFilterResponseModel>()
.Map(dest => dest.CreatedAt, src => src.Created);
config.NewConfig<UserWalletChangeLog, GetUserWalletChangeLogResponseDto>()
.Map(dest => dest.CreatedAt, src => src.Created);
}
}
@@ -0,0 +1,16 @@
using CMSMicroservice.Application.UserWalletHistoryCQ.Queries.GetAllUserWalletHistoryByFilter;
using CMSMicroservice.Application.UserWalletHistoryCQ.Queries.GetUserWalletHistory;
namespace CMSMicroservice.Application.Common.Mappings;
public class UserWalletHistoryProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
config.NewConfig<UserWalletHistory,GetAllUserWalletHistoryByFilterResponseModel>()
.Map(dest => dest.CreatedAt, src => src.Created);
config.NewConfig<UserWalletHistory, GetUserWalletHistoryResponseDto>()
.Map(dest => dest.CreatedAt, src => src.Created);
}
}
@@ -12,7 +12,7 @@ namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckAndProcessDayaLoa
/// 1. استعلام از API دایا /// 1. استعلام از API دایا
/// 2. ذخیره/به‌روزرسانی DayaLoanContract /// 2. ذخیره/به‌روزرسانی DayaLoanContract
/// 3. شارژ کیف پول برای وام‌های تأیید شده /// 3. شارژ کیف پول برای وام‌های تأیید شده
/// 4. ثبت Order پکیج طلایی /// 4. ثبت Order پکیج
/// 5. ارسال SMS /// 5. ارسال SMS
/// </summary> /// </summary>
public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndProcessDayaLoansCommand, CheckAndProcessDayaLoansResponseDto> public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndProcessDayaLoansCommand, CheckAndProcessDayaLoansResponseDto>
@@ -122,6 +122,7 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
contract.IsProcessed = true; contract.IsProcessed = true;
result.WasProcessed = true; result.WasProcessed = true;
result.NewWalletBalance = processResult.NewBalance; result.NewWalletBalance = processResult.NewBalance;
result.PackageName = processResult.PackageName;
result.Message = "وام پردازش و کیف پول شارژ شد"; result.Message = "وام پردازش و کیف پول شارژ شد";
processedCount++; processedCount++;
@@ -181,15 +182,20 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
/// <summary> /// <summary>
/// پردازش مالی وام دایا /// پردازش مالی وام دایا
/// </summary> /// </summary>
private async Task<(long NewBalance, long TransactionId)> ProcessDayaLoanAsync( private async Task<(long NewBalance, long TransactionId, string PackageName)> ProcessDayaLoanAsync(
User user, User user,
string contractNumber, string contractNumber,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// بارگذاری پکیج پایه
var package = await _context.Packages
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
?? throw new InvalidOperationException("پکیج پایه یافت نشد");
// 1. ایجاد تراکنش // 1. ایجاد تراکنش
var transaction = new Transaction var transaction = new Transaction
{ {
Amount = SystemConstants.DayaLoanAmount, Amount = package.Price,
Description = $"دریافت اعتبار دایا - قرارداد {contractNumber}", Description = $"دریافت اعتبار دایا - قرارداد {contractNumber}",
PaymentStatus = PaymentStatus.Success, PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.Now, PaymentDate = DateTime.Now,
@@ -216,25 +222,26 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
} }
// 3. شارژ کیف پول عادی // 3. شارژ کیف پول عادی
wallet.Balance += SystemConstants.DayaLoanAmount; wallet.Balance += package.Price;
var mainLog = new UserWalletChangeLog var mainLog = new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = 0, CurrentBalance = 0,
ChangeValue = SystemConstants.DayaLoanAmount, ChangeValue = package.Price,
CurrentNetworkBalance = wallet.NetworkBalance, CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0, ChangeNerworkValue = 0,
IsIncrease = true, IsIncrease = true,
RefrenceId = transaction.Id RefrenceId = transaction.Id,
PackageId = package.Id
}; };
await _context.UserWalletChangeLogs.AddAsync(mainLog, cancellationToken); await _context.UserWalletHistories.AddAsync(mainLog, cancellationToken);
// 4. شارژ کیف پول تخفیف (دو برابر) // 4. شارژ کیف پول تخفیف
var discountAmount = SystemConstants.DayaLoanAmount * 2; var discountAmount = (long)(package.Price * package.DiscountMultiplier);
wallet.DiscountBalance += discountAmount; wallet.DiscountBalance += discountAmount;
var discountLog = new UserWalletChangeLog var discountLog = new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = wallet.Balance, CurrentBalance = wallet.Balance,
@@ -244,62 +251,54 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
CurrentDiscountBalance = 0, CurrentDiscountBalance = 0,
ChangeDiscountValue = discountAmount, ChangeDiscountValue = discountAmount,
IsIncrease = true, IsIncrease = true,
RefrenceId = transaction.Id RefrenceId = transaction.Id,
PackageId = package.Id
}; };
await _context.UserWalletChangeLogs.AddAsync(discountLog, cancellationToken); await _context.UserWalletHistories.AddAsync(discountLog, cancellationToken);
// 5. به‌روزرسانی کاربر // 5. به‌روزرسانی کاربر
user.HasReceivedDayaCredit = true; user.HasReceivedDayaCredit = true;
user.DayaCreditReceivedAt = DateTime.Now; user.DayaCreditReceivedAt = DateTime.Now;
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan; user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan;
// 6. ثبت Order پکیج پایه
var goldenPackage = await _context.Packages
.FirstOrDefaultAsync(p => p.Id == 4, cancellationToken);
if (goldenPackage != null)
{
// 6. ثبت UserPackagePurchase برای پکیج پایه // 6. ثبت UserPackagePurchase برای پکیج پایه
var goldenPackageId =goldenPackage.Id;
var packagePurchase = new UserPackagePurchase var packagePurchase = new UserPackagePurchase
{ {
UserId = user.Id, UserId = user.Id,
PackageId = goldenPackageId, PackageId = package.Id,
PurchaseMethod = PackagePurchaseMethod.DayaLoan, PurchaseMethod = PackagePurchaseMethod.DayaLoan,
PurchasedAt = DateTime.Now, PurchasedAt = DateTime.Now,
Amount = SystemConstants.DayaLoanAmount, Amount = package.Price,
TransactionId = transaction.Id TransactionId = transaction.Id
}; };
await _context.UserPackagePurchases.AddAsync(packagePurchase, cancellationToken); await _context.UserPackagePurchases.AddAsync(packagePurchase, cancellationToken);
}
// 7. Domain Event // 7. Domain Event
user.AddDomainEvent(new DayaLoanApprovedEvent(user, transaction, contractNumber)); user.AddDomainEvent(new DayaLoanApprovedEvent(user, transaction, contractNumber));
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
// 8. ارسال SMS // 8. ارسال SMS
await SendDayaLoanSmsAsync(user); await SendDayaLoanSmsAsync(user, package.Title);
return (wallet.Balance, transaction.Id); return (wallet.Balance, transaction.Id, package.Title);
} }
/// <summary> /// <summary>
/// ارسال SMS اطلاع‌رسانی /// ارسال SMS اطلاع‌رسانی
/// </summary> /// </summary>
private async Task SendDayaLoanSmsAsync(User user) private async Task SendDayaLoanSmsAsync(User user, string packageName)
{ {
try try
{ {
var userName = SmsTemplates.GetUserName(user.FirstName+" "+user.LastName); var userName = SmsTemplates.GetUserName(user.FirstName+" "+user.LastName);
var message = SmsTemplates.DayaLoanReceived(userName); var message = SmsTemplates.DayaLoanReceived(userName, packageName);
await _smsService.SendAsync(user.Mobile, message); await _smsService.SendAsync(user.Mobile, message);
// ارسال SMS به ادمین برای اطلاع // ارسال SMS به ادمین برای اطلاع
var adminMessage = $"وام دایا دریافت شد\nکاربر: {user.FirstName} {user.LastName}\nموبایل: {user.Mobile}\nکدملی: {user.NationalCode}"; var adminMessage = $"وام دایا دریافت شد\nکاربر: {user.FirstName} {user.LastName}\nموبایل: {user.Mobile}\nکدملی: {user.NationalCode}\nپکیج: {packageName}";
await _smsService.SendAsync("09199877503", adminMessage); await _smsService.SendAsync("09199877503", adminMessage);
_logger.LogInformation("Daya loan SMS sent to User {UserId}", user.Id); _logger.LogInformation("Daya loan SMS sent to User {UserId}", user.Id);
@@ -63,6 +63,11 @@ public class DayaLoanProcessResult
/// </summary> /// </summary>
public long? NewWalletBalance { get; set; } public long? NewWalletBalance { get; set; }
/// <summary>
/// نام پکیج
/// </summary>
public string? PackageName { get; set; }
/// <summary> /// <summary>
/// پیام /// پیام
/// </summary> /// </summary>
@@ -67,7 +67,7 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
userWallet.DiscountBalance -= order.DiscountBalanceUsed; userWallet.DiscountBalance -= order.DiscountBalanceUsed;
// ثبت لاگ تغییرات کیف پول // ثبت لاگ تغییرات کیف پول
_context.UserWalletChangeLogs.Add(new Domain.Entities.UserWalletChangeLog _context.UserWalletHistories.Add(new Domain.Entities.UserWalletHistory
{ {
WalletId = userWallet.Id, WalletId = userWallet.Id,
CurrentBalance = userWallet.Balance, CurrentBalance = userWallet.Balance,
@@ -118,7 +118,7 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
return new PlaceOrderResponseDto return new PlaceOrderResponseDto
{ {
Success = false, Success = false,
Message = $"موجودی کیف پول اعتباری کافی نیست. مبلغ مورد نیاز: {maxDiscountBalanceUsable:N0} ریال، موجودی شما: {userWallet.DiscountBalance:N0} ریال" Message = $"موجودی کیف پول اعتباری کافی نیست. مبلغ مورد نیاز: {maxDiscountBalanceUsable:N0} تومان، موجودی شما: {userWallet.DiscountBalance:N0} تومان"
}; };
} }
@@ -195,9 +195,9 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
{ {
try try
{ {
// آدرس callback — زرین‌پال بعد از پرداخت کاربر را به اینجا هدایت می‌کند // آدرس callback — زرین‌پال بعد از پرداخت مستقیم به فرانت‌آفیس هدایت می‌کند
var cmsBaseUrl = _configuration["CmsBaseUrl"] ?? "https://localhost:32846"; var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
var callbackUrl = $"{cmsBaseUrl}/api/payment/discount-order/callback?orderId={order.Id}"; var callbackUrl = $"{frontOfficeBaseUrl}/profile/payment-callback?type=discount-order&orderId={order.Id}";
// درخواست به درگاه // درخواست به درگاه
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
@@ -279,7 +279,7 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
walletForDeduct.DiscountBalance -= actualDiscountBalanceUsed; walletForDeduct.DiscountBalance -= actualDiscountBalanceUsed;
// ثبت لاگ تغییرات کیف پول // ثبت لاگ تغییرات کیف پول
_context.UserWalletChangeLogs.Add(new Domain.Entities.UserWalletChangeLog _context.UserWalletHistories.Add(new Domain.Entities.UserWalletHistory
{ {
WalletId = walletForDeduct.Id, WalletId = walletForDeduct.Id,
CurrentBalance = walletForDeduct.Balance, CurrentBalance = walletForDeduct.Balance,
@@ -108,7 +108,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
wallet.DiscountBalance += manualPayment.Amount; wallet.DiscountBalance += manualPayment.Amount;
// لاگ Balance // لاگ Balance
await _context.UserWalletChangeLogs.AddAsync(new UserWalletChangeLog await _context.UserWalletHistories.AddAsync(new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = wallet.Balance, CurrentBalance = wallet.Balance,
@@ -122,7 +122,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
}, cancellationToken); }, cancellationToken);
// لاگ DiscountBalance // لاگ DiscountBalance
await _context.UserWalletChangeLogs.AddAsync(new UserWalletChangeLog await _context.UserWalletHistories.AddAsync(new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = wallet.Balance, CurrentBalance = wallet.Balance,
@@ -139,7 +139,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
case ManualPaymentType.DiscountWalletCharge: case ManualPaymentType.DiscountWalletCharge:
wallet.DiscountBalance += manualPayment.Amount; wallet.DiscountBalance += manualPayment.Amount;
await _context.UserWalletChangeLogs.AddAsync(new UserWalletChangeLog await _context.UserWalletHistories.AddAsync(new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = wallet.Balance, CurrentBalance = wallet.Balance,
@@ -156,7 +156,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
case ManualPaymentType.NetworkWalletCharge: case ManualPaymentType.NetworkWalletCharge:
wallet.NetworkBalance += manualPayment.Amount; wallet.NetworkBalance += manualPayment.Amount;
await _context.UserWalletChangeLogs.AddAsync(new UserWalletChangeLog await _context.UserWalletHistories.AddAsync(new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = wallet.Balance, CurrentBalance = wallet.Balance,
@@ -183,7 +183,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
wallet.DiscountBalance -= manualPayment.Amount; wallet.DiscountBalance -= manualPayment.Amount;
} }
await _context.UserWalletChangeLogs.AddAsync(new UserWalletChangeLog await _context.UserWalletHistories.AddAsync(new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = wallet.Balance, CurrentBalance = wallet.Balance,
@@ -201,7 +201,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
// Other یا سایر موارد - فقط Balance // Other یا سایر موارد - فقط Balance
wallet.Balance += manualPayment.Amount; wallet.Balance += manualPayment.Amount;
await _context.UserWalletChangeLogs.AddAsync(new UserWalletChangeLog await _context.UserWalletHistories.AddAsync(new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = wallet.Balance, CurrentBalance = wallet.Balance,
@@ -14,7 +14,7 @@ public class CreateManualPaymentCommand : IRequest<long>
public long UserId { get; set; } public long UserId { get; set; }
/// <summary> /// <summary>
/// مبلغ تراکنش (ریال) /// مبلغ تراکنش (تومان)
/// </summary> /// </summary>
public long Amount { get; set; } public long Amount { get; set; }
@@ -70,9 +70,13 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
throw new NotFoundException($"کیف پول کاربر {request.UserId} یافت نشد"); throw new NotFoundException($"کیف پول کاربر {request.UserId} یافت نشد");
} }
// 4. محاسبه مبالغ // 4. بارگذاری پکیج پایه و محاسبه مبالغ
var balanceAmount = SystemConstants.BasePackageAmount; // 56M var package = await _context.Packages
var discountBalanceAmount = SystemConstants.BasePackageAmount * 2; // 112M .FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
?? throw new NotFoundException("پکیج پایه یافت نشد");
var balanceAmount = package.Price;
var discountBalanceAmount = (long)(package.Price * package.DiscountMultiplier);
// 5. ثبت تراکنش // 5. ثبت تراکنش
@@ -112,11 +116,11 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
var oldBalance = wallet.Balance; var oldBalance = wallet.Balance;
var oldDiscountBalance = wallet.DiscountBalance; var oldDiscountBalance = wallet.DiscountBalance;
wallet.Balance += balanceAmount; // +56M wallet.Balance += balanceAmount;
wallet.DiscountBalance += discountBalanceAmount; // +112M wallet.DiscountBalance += discountBalanceAmount;
// 8. ثبت لاگ کیف پول // 8. ثبت لاگ کیف پول
var walletLog = new UserWalletChangeLog var walletLog = new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = 0, CurrentBalance = 0,
@@ -126,10 +130,11 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
CurrentDiscountBalance =0, CurrentDiscountBalance =0,
ChangeDiscountValue = discountBalanceAmount, ChangeDiscountValue = discountBalanceAmount,
IsIncrease = true, IsIncrease = true,
RefrenceId = transaction.Id RefrenceId = transaction.Id,
PackageId = package.Id
}; };
await _context.UserWalletChangeLogs.AddAsync(walletLog, cancellationToken); await _context.UserWalletHistories.AddAsync(walletLog, cancellationToken);
// 9. تنظیم روش خرید پکیج // 9. تنظیم روش خرید پکیج
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase; user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
@@ -1,3 +1,4 @@
using CMSMicroservice.Domain.Common;
using FluentValidation; using FluentValidation;
namespace CMSMicroservice.Application.ManualPaymentCQ.Commands.CreateManualPayment; namespace CMSMicroservice.Application.ManualPaymentCQ.Commands.CreateManualPayment;
@@ -10,11 +11,12 @@ public class CreateManualPaymentCommandValidator : AbstractValidator<CreateManua
.GreaterThan(0) .GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد"); .WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
// حصار ایمنی — مبلغ واقعی از پکیج کاربر خوانده می‌شود
RuleFor(x => x.Amount) RuleFor(x => x.Amount)
.GreaterThan(0) .GreaterThan(0)
.WithMessage("مبلغ باید بزرگتر از صفر باشد") .WithMessage("مبلغ باید بزرگتر از صفر باشد")
.LessThanOrEqualTo(1_000_000_000) .LessThanOrEqualTo(SystemConstants.WalletMaxSafeAmount)
.WithMessage("مبلغ نمی‌تواند بیشتر از 1 میلیارد ریال باشد"); .WithMessage("مبلغ وارد شده بیش از حد مجاز است");
RuleFor(x => x.Type) RuleFor(x => x.Type)
.IsInEnum() .IsInEnum()
@@ -8,7 +8,7 @@ public record ProcessManualMembershipPaymentCommand : IRequest<ProcessManualMemb
public long UserId { get; init; } public long UserId { get; init; }
/// <summary> /// <summary>
/// مبلغ پرداختی (ریال) /// مبلغ پرداختی (تومان)
/// </summary> /// </summary>
public long Amount { get; init; } public long Amount { get; init; }
@@ -104,7 +104,7 @@ public class ProcessManualMembershipPaymentCommandHandler : IRequestHandler<Proc
wallet.DiscountBalance += request.Amount; wallet.DiscountBalance += request.Amount;
// 8. ثبت لاگ Balance // 8. ثبت لاگ Balance
var balanceLog = new UserWalletChangeLog var balanceLog = new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = wallet.Balance, CurrentBalance = wallet.Balance,
@@ -116,7 +116,7 @@ public class ProcessManualMembershipPaymentCommandHandler : IRequestHandler<Proc
IsIncrease = true, IsIncrease = true,
RefrenceId = transaction.Id RefrenceId = transaction.Id
}; };
await _context.UserWalletChangeLogs.AddAsync(balanceLog, cancellationToken); await _context.UserWalletHistories.AddAsync(balanceLog, cancellationToken);
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase; user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
// 10. به‌روزرسانی ManualPayment با TransactionId // 10. به‌روزرسانی ManualPayment با TransactionId
@@ -69,6 +69,14 @@ public class GetAllManualPaymentsQueryHandler
// Pagination // Pagination
var skip = (request.PageNumber - 1) * request.PageSize; var skip = (request.PageNumber - 1) * request.PageSize;
// دریافت PackageName از آخرین خرید پکیج کاربر
var userPackageNames = await _context.UserPackagePurchases
.Include(p => p.Package)
.GroupBy(p => p.UserId)
.Select(g => new { UserId = g.Key, PackageName = g.OrderByDescending(x => x.PurchasedAt).First().Package.Title })
.ToDictionaryAsync(x => x.UserId, x => x.PackageName, cancellationToken);
var data = await query var data = await query
.Skip(skip) .Skip(skip)
.Take(request.PageSize) .Take(request.PageSize)
@@ -96,6 +104,13 @@ public class GetAllManualPaymentsQueryHandler
}) })
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
// تکمیل PackageName
foreach (var item in data)
{
if (userPackageNames.TryGetValue(item.UserId, out var pkgName))
item.PackageName = pkgName;
}
var metaData = new MetaData var metaData = new MetaData
{ {
TotalCount = totalCount, TotalCount = totalCount,
@@ -30,4 +30,5 @@ public class ManualPaymentDto
public string? RejectionReason { get; set; } public string? RejectionReason { get; set; }
public long? TransactionId { get; set; } public long? TransactionId { get; set; }
public DateTime Created { get; set; } public DateTime Created { get; set; }
public string? PackageName { get; set; }
} }
@@ -143,7 +143,7 @@ public class GetUserNetworkPositionQueryHandler : IRequestHandler<GetUserNetwork
HasReceivedDayaCredit = user.HasReceivedDayaCredit, HasReceivedDayaCredit = user.HasReceivedDayaCredit,
DayaCreditReceivedAt = user.DayaCreditReceivedAt, DayaCreditReceivedAt = user.DayaCreditReceivedAt,
PackagePurchaseMethod = user.PackagePurchaseMethod, PackagePurchaseMethod = user.PackagePurchaseMethod,
HasPurchasedGoldenPackage = user.PackagePurchaseMethod != PackagePurchaseMethod.None, HasPurchasedPackage = user.PackagePurchaseMethod != PackagePurchaseMethod.None,
// آمار مالی // آمار مالی
TotalEarnedCommission = commissionStats?.TotalAmount ?? 0, TotalEarnedCommission = commissionStats?.TotalAmount ?? 0,
@@ -49,7 +49,7 @@ public class UserNetworkPositionDto
public bool HasReceivedDayaCredit { get; set; } public bool HasReceivedDayaCredit { get; set; }
public DateTime? DayaCreditReceivedAt { get; set; } public DateTime? DayaCreditReceivedAt { get; set; }
public PackagePurchaseMethod PackagePurchaseMethod { get; set; } public PackagePurchaseMethod PackagePurchaseMethod { get; set; }
public bool HasPurchasedGoldenPackage { get; set; } public bool HasPurchasedPackage { get; set; }
// آمار مالی // آمار مالی
public decimal TotalEarnedCommission { get; set; } public decimal TotalEarnedCommission { get; set; }
@@ -60,7 +60,7 @@ public class CancelOrderByAdminCommandHandler : IRequestHandler<CancelOrderByAdm
var wallet = order.User.UserWallets.FirstOrDefault(); var wallet = order.User.UserWallets.FirstOrDefault();
if (wallet != null) if (wallet != null)
{ {
var walletLog = new UserWalletChangeLog var walletLog = new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = wallet.Balance, CurrentBalance = wallet.Balance,
@@ -74,7 +74,7 @@ public class CancelOrderByAdminCommandHandler : IRequestHandler<CancelOrderByAdm
wallet.Balance += order.Amount; wallet.Balance += order.Amount;
await _context.UserWalletChangeLogs.AddAsync(walletLog, cancellationToken); await _context.UserWalletHistories.AddAsync(walletLog, cancellationToken);
_logger.LogInformation( _logger.LogInformation(
"Refund processed. OrderId: {OrderId}, Amount: {Amount}, UserId: {UserId}", "Refund processed. OrderId: {OrderId}, Amount: {Amount}, UserId: {UserId}",
@@ -26,12 +26,12 @@ public class CreateNewOtpTokenEventHandler : INotificationHandler<CreateNewOtpTo
{ {
var purpose = notification.Item.Purpose?.ToLowerInvariant(); var purpose = notification.Item.Purpose?.ToLowerInvariant();
// برای امضای قرارداد، پیامک ساده با GUID ارسال شود // برای امضای قرارداد، از الگوی Sign-Contract با GUID به عنوان token2 استفاده شود
if ((purpose == "signcontract" || purpose == "signclubcontract") if ((purpose == "signcontract" || purpose == "signclubcontract")
&& !string.IsNullOrEmpty(notification.SignGuid)) && !string.IsNullOrEmpty(notification.SignGuid))
{ {
var message = $"کد تایید امضای قرارداد: {notification.PlainCode}\nشناسه قرارداد: {notification.SignGuid}"; await _kavenegarService.VerifyLookupAsync(
await _kavenegarService.SendAsync(notification.Item.Mobile, message); notification.Item.Mobile, notification.PlainCode, notification.SignGuid, "Sign-Contract");
} }
else else
{ {
@@ -9,5 +9,18 @@ public record CreateNewPackageCommand : IRequest<CreateNewPackageResponseDto>
public string ImagePath { get; init; } public string ImagePath { get; init; }
//قیمت //قیمت
public long Price { 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); await _context.Packages.AddAsync(entity, cancellationToken);
entity.AddDomainEvent(new CreateNewPackageEvent(entity)); entity.AddDomainEvent(new CreateNewPackageEvent(entity));
await _context.SaveChangesAsync(cancellationToken); 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>(); 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 string ImagePath { get; init; }
//قیمت //قیمت
public long Price { 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); request.Adapt(entity);
_context.Packages.Update(entity); _context.Packages.Update(entity);
entity.AddDomainEvent(new UpdatePackageEvent(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); await _context.SaveChangesAsync(cancellationToken);
return Unit.Value; 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 باشد");
}
}
@@ -58,10 +58,18 @@ public class VerifyPackagePurchaseCommandHandler
return true; return true;
} }
// 3. Verify با درگاه بانکی // واکشی PaymentTransaction برای گرفتن مبلغ (تومان) جهت verify
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, cancellationToken);
var amountInToman = (decimal)(paymentTx?.Amount ?? order.Amount);
// 3. Verify با درگاه بانکی (مبلغ به تومان — تبدیل به ریال در ZarinPalService)
var verifyResult = await _paymentGateway.VerifyPaymentAsync( var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority, request.Authority,
request.Authority // verificationToken - در بعضی درگاه‌ها همان Authority است "OK", // verificationToken — Status از درگاه
amountInToman,
cancellationToken
); );
if (!verifyResult.IsSuccess) if (!verifyResult.IsSuccess)
@@ -99,9 +107,13 @@ public class VerifyPackagePurchaseCommandHandler
wallet.Balance wallet.Balance
); );
// شارژ DiscountBalance (موجودی تخفیف) — دو برابر مبلغ سفارش // شارژ DiscountBalance (موجودی تخفیف) — ضریب تخفیف از پکیج
var oldDiscountBalance = wallet.DiscountBalance; var oldDiscountBalance = wallet.DiscountBalance;
wallet.DiscountBalance += order.Amount * 2; if (order.Package == null)
throw new NotFoundException("پکیج مرتبط با سفارش یافت نشد");
var discountMultiplier = order.Package.DiscountMultiplier;
var discountAmount = (long)(order.Amount * discountMultiplier);
wallet.DiscountBalance += discountAmount;
_logger.LogInformation( _logger.LogInformation(
"Charging DiscountBalance for UserId {UserId}: {OldBalance} -> {NewBalance}", "Charging DiscountBalance for UserId {UserId}: {OldBalance} -> {NewBalance}",
@@ -125,22 +137,23 @@ public class VerifyPackagePurchaseCommandHandler
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
// 6. ثبت لاگ تغییر Balance // 6. ثبت لاگ تغییر Balance
var balanceLog = new UserWalletChangeLog var balanceLog = new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = wallet.Balance, CurrentBalance = wallet.Balance,
ChangeValue = order.Amount, ChangeValue = order.Amount,
CurrentNetworkBalance = wallet.NetworkBalance, CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0, ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance - (order.Amount * 2), // قبل از شارژ DiscountBalance CurrentDiscountBalance = wallet.DiscountBalance - discountAmount, // قبل از شارژ DiscountBalance
ChangeDiscountValue = 0, ChangeDiscountValue = 0,
IsIncrease = true, IsIncrease = true,
RefrenceId = transaction.Id RefrenceId = transaction.Id,
PackageId = order.PackageId
}; };
await _context.UserWalletChangeLogs.AddAsync(balanceLog, cancellationToken); await _context.UserWalletHistories.AddAsync(balanceLog, cancellationToken);
// 7. ثبت لاگ تغییر DiscountBalance // 7. ثبت لاگ تغییر DiscountBalance
var discountLog = new UserWalletChangeLog var discountLog = new UserWalletHistory
{ {
WalletId = wallet.Id, WalletId = wallet.Id,
CurrentBalance = wallet.Balance, CurrentBalance = wallet.Balance,
@@ -148,19 +161,40 @@ public class VerifyPackagePurchaseCommandHandler
CurrentNetworkBalance = wallet.NetworkBalance, CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0, ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance, CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = order.Amount * 2, ChangeDiscountValue = discountAmount,
IsIncrease = true, IsIncrease = true,
RefrenceId = transaction.Id RefrenceId = transaction.Id,
PackageId = order.PackageId
}; };
await _context.UserWalletChangeLogs.AddAsync(discountLog, cancellationToken); await _context.UserWalletHistories.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.TransactionId = transaction.Id;
order.PaymentStatus = PaymentStatus.Success; order.PaymentStatus = PaymentStatus.Success;
order.PaymentDate = DateTime.Now; order.PaymentDate = DateTime.Now;
order.PaymentMethod = PaymentMethod.IPG; order.PaymentMethod = PaymentMethod.IPG;
// 9. تغییر User.PackagePurchaseMethod // 10. تغییر User.PackagePurchaseMethod
order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase; order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
@@ -11,8 +11,10 @@ public class GetAllPackageByFilterQueryHandler : IRequestHandler<GetAllPackageBy
public async Task<GetAllPackageByFilterResponseDto> Handle(GetAllPackageByFilterQuery request, CancellationToken cancellationToken) public async Task<GetAllPackageByFilterResponseDto> Handle(GetAllPackageByFilterQuery request, CancellationToken cancellationToken)
{ {
var query = _context.Packages var query = _context.Packages
.Where(x => !x.IsDeleted)
.ApplyOrder(sortBy: request.SortBy) .ApplyOrder(sortBy: request.SortBy)
.AsNoTracking() .AsNoTracking()
.Include(x => x.PackageFeatures)
.AsQueryable(); .AsQueryable();
if (request.Filter is not null) 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) .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 return new GetAllPackageByFilterResponseDto
{ {
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken), MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
Models = await query.PaginatedListAsync(paginationState: request.PaginationState) Models = models
.ProjectToType<GetAllPackageByFilterResponseModel>().ToListAsync(cancellationToken)
}; };
} }
} }
@@ -18,4 +18,18 @@ public class GetAllPackageByFilterResponseDto
public string ImagePath { get; set; } public string ImagePath { get; set; }
//قیمت //قیمت
public long Price { 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 var package = await _context.Packages
.AsNoTracking() .AsNoTracking()
.Where(x => x.Id == request.PackageId) .Include(p => p.PackageFeatures)
.ProjectToType<GetCustomerPackageDetailsResponseDto>() .ThenInclude(pf => pf.ClubFeature)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(x => x.Id == request.PackageId && !x.IsDeleted, cancellationToken);
if (package == null) if (package == null)
throw new NotFoundException(nameof(Package), request.PackageId); throw new NotFoundException(nameof(Package), request.PackageId);
// Add features based on package (this could be stored in DB in future) var response = new GetCustomerPackageDetailsResponseDto
package.Features = new List<PackageFeatureDto>
{ {
new PackageFeatureDto Id = package.Id,
{ Title = package.Title,
Title = "درآمد کمیسیون", Description = package.Description,
Description = "دریافت کمیسیون از فروش محصولات", Price = package.Price,
Icon = "commission", ImagePath = package.ImagePath,
IsHighlighted = true ActivationFee = package.ActivationFee,
}, DiscountMultiplier = (double)package.DiscountMultiplier,
new PackageFeatureDto MagicWalletMultiplier = (double)package.MagicWalletMultiplier,
{ MagicWalletMaxDeposit = package.MagicWalletMaxDeposit,
Title = "پشتیبانی 24/7", MagicWalletMaxCredit = package.MagicWalletMaxCredit,
Description = "دسترسی به پشتیبانی در تمام ساعات شبانه روز", IsBasePackage = package.IsBasePackage,
Icon = "support", SupportsDayaPurchase = package.SupportsDayaPurchase,
IsHighlighted = false SupportsDirectPurchase = package.SupportsDirectPurchase
},
new PackageFeatureDto
{
Title = "آموزش‌های تخصصی",
Description = "دسترسی به دوره‌های آموزشی و وبینارها",
Icon = "education",
IsHighlighted = true
}
}; };
// Set purchase requirements // بارگذاری ویژگی‌ها از DB (PackageFeatures → ClubFeature)
package.Requirements = new PurchaseRequirementsDto 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, RequiresMembership = false,
MinimumWalletBalance = package.Price / 10, // 10% minimum MinimumWalletBalance = package.Price / 10,
Restrictions = new List<string> Restrictions = new List<string>
{ {
"باید حداقل 18 سال سن داشته باشید", "باید حداقل 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 string ImagePath { get; set; }
public List<PackageFeatureDto> Features { get; set; } = new(); public List<PackageFeatureDto> Features { get; set; } = new();
public PurchaseRequirementsDto Requirements { get; set; } 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 public class PackageFeatureDto
@@ -17,35 +17,43 @@ public class GetCustomerPackagesQueryHandler : IRequestHandler<GetCustomerPackag
{ {
var query = _context.Packages var query = _context.Packages
.AsNoTracking() .AsNoTracking()
.Where(p => !p.IsDeleted)
.AsQueryable(); .AsQueryable();
// Filter by PackageType if specified // فیلتر IsActive
if (request.PackageTypeFilter.HasValue) 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 Id = p.Id,
// Assuming Title or Description contains the package type indicator Name = p.Title,
// If Package entity needs PackageType field, it should be added to migration Title = p.Title,
} Description = p.Description,
Price = p.Price,
// Get all packages (assuming all are available unless marked otherwise) ImageUrl = p.ImagePath,
var packages = await query ImagePath = p.ImagePath,
.ProjectToType<GetCustomerPackagesResponseDto>() Currency = "IRR",
.ToListAsync(cancellationToken); IsAvailable = p.IsActive,
ValidityDays = 365,
// Map additional fields IsPopular = p.IsBasePackage,
foreach (var package in packages) ShortDescription = p.Description?.Length > 100
{ ? p.Description.Substring(0, 100) + "..."
package.Name = package.Title; : p.Description,
package.ImageUrl = package.ImagePath; // New fields
package.Currency = "IRR"; ActivationFee = p.ActivationFee,
package.IsAvailable = true; DiscountMultiplier = (double)p.DiscountMultiplier,
package.ValidityDays = 365; // Default validity MagicWalletMultiplier = (double)p.MagicWalletMultiplier,
package.IsPopular = false; MagicWalletMaxDeposit = p.MagicWalletMaxDeposit,
package.ShortDescription = package.Description?.Length > 100 MagicWalletMaxCredit = p.MagicWalletMaxCredit,
? package.Description.Substring(0, 100) + "..." IsBasePackage = p.IsBasePackage,
: package.Description; SupportsDayaPurchase = p.SupportsDayaPurchase,
} SupportsDirectPurchase = p.SupportsDirectPurchase
}).ToList();
return packages;
} }
} }
@@ -15,4 +15,13 @@ public class GetCustomerPackagesResponseDto
public int ValidityDays { get; set; } public int ValidityDays { get; set; }
public bool IsPopular { get; set; } public bool IsPopular { get; set; }
public string ShortDescription { 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() .AsNoTracking()
.Where(x => x.UserId == userId && x.PackageId != null) .Where(x => x.UserId == userId && x.PackageId != null)
.Include(x => x.Package) .Include(x => x.Package)
.Include(x => x.Transaction)
.AsQueryable(); .AsQueryable();
// Apply date filters if specified // Apply date filters if specified
@@ -11,12 +11,19 @@ public class GetPackageQueryHandler : IRequestHandler<GetPackageQuery, GetPackag
public async Task<GetPackageResponseDto> Handle(GetPackageQuery request, public async Task<GetPackageResponseDto> Handle(GetPackageQuery request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var response = await _context.Packages var package = await _context.Packages
.AsNoTracking() .AsNoTracking()
.Include(x => x.PackageFeatures)
.Where(x => x.Id == request.Id) .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 string ImagePath { get; set; }
//قیمت //قیمت
public long Price { 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.Application.Common.Interfaces;
using CMSMicroservice.Domain.Enums; using CMSMicroservice.Domain.Enums;
using MediatR; using MediatR;
@@ -15,47 +16,49 @@ public class GetUserPackageStatusQueryHandler : IRequestHandler<GetUserPackageSt
public async Task<UserPackageStatusDto> Handle(GetUserPackageStatusQuery request, CancellationToken cancellationToken) public async Task<UserPackageStatusDto> Handle(GetUserPackageStatusQuery request, CancellationToken cancellationToken)
{ {
// TODO: پیاده‌سازی دریافت وضعیت پکیج کاربر // 1. دریافت اطلاعات کاربر
// var user = await _context.Users
// 1. دریافت اطلاعات کاربر: .Include(u => u.UserWallets)
// - var user = await _context.Users .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
// .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 مفید است تا وضعیت کاربر را نمایش دهد
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
};
} }
} }
@@ -72,11 +72,10 @@ public class CreateNewOtpTokenCommandHandler : IRequestHandler<CreateNewOtpToken
try try
{ {
// برای امضای قرارداد، شناسه GUID هم در پیامک ارسال شود // برای امضای قرارداد، از الگوی Sign-Contract با GUID به عنوان token2 استفاده شود
if ((purpose == "signcontract" || purpose == "signclubcontract") && !string.IsNullOrEmpty(request.SignGuid)) if ((purpose == "signcontract" || purpose == "signclubcontract") && !string.IsNullOrEmpty(request.SignGuid))
{ {
var message = $"کد تایید امضای قرارداد: {code}\nشناسه قرارداد: {request.SignGuid}"; await _kavenegarService.VerifyLookupAsync(mobile, code, request.SignGuid, "Sign-Contract");
await _kavenegarService.SendAsync(mobile, message);
} }
else else
{ {
@@ -60,7 +60,7 @@ public class GetCustomerReferralsQueryHandler : IRequestHandler<GetCustomerRefer
// Calculate this month's commission from wallet changelog // Calculate this month's commission from wallet changelog
var startOfMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); var startOfMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
var thisMonthCommission = await _context.UserWalletChangeLogs var thisMonthCommission = await _context.UserWalletHistories
.AsNoTracking() .AsNoTracking()
.Include(x => x.Wallet) .Include(x => x.Wallet)
.Where(x => x.Wallet.UserId == userId && x.Created >= startOfMonth) .Where(x => x.Wallet.UserId == userId && x.Created >= startOfMonth)
@@ -1,6 +1,6 @@
namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletChangeLog; namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory;
public class GetCustomerWalletChangeLogQuery : IRequest<List<GetCustomerWalletChangeLogResponseDto>> public class GetCustomerWalletHistoryQuery : IRequest<List<GetCustomerWalletHistoryResponseDto>>
{ {
/// <summary> /// <summary>
/// فیلتر بر اساس شناسه ارجاع (اختیاری) /// فیلتر بر اساس شناسه ارجاع (اختیاری)
@@ -1,11 +1,11 @@
namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletChangeLog; namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory;
public class GetCustomerWalletChangeLogQueryHandler : IRequestHandler<GetCustomerWalletChangeLogQuery, List<GetCustomerWalletChangeLogResponseDto>> public class GetCustomerWalletHistoryQueryHandler : IRequestHandler<GetCustomerWalletHistoryQuery, List<GetCustomerWalletHistoryResponseDto>>
{ {
private readonly IApplicationDbContext _context; private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser; private readonly ICurrentUserService _currentUser;
public GetCustomerWalletChangeLogQueryHandler( public GetCustomerWalletHistoryQueryHandler(
IApplicationDbContext context, IApplicationDbContext context,
ICurrentUserService currentUser) ICurrentUserService currentUser)
{ {
@@ -13,8 +13,8 @@ public class GetCustomerWalletChangeLogQueryHandler : IRequestHandler<GetCustome
_currentUser = currentUser; _currentUser = currentUser;
} }
public async Task<List<GetCustomerWalletChangeLogResponseDto>> Handle( public async Task<List<GetCustomerWalletHistoryResponseDto>> Handle(
GetCustomerWalletChangeLogQuery request, GetCustomerWalletHistoryQuery request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// Get current user's ID from JWT // Get current user's ID from JWT
@@ -35,7 +35,7 @@ public class GetCustomerWalletChangeLogQueryHandler : IRequestHandler<GetCustome
} }
// Build query for wallet change logs // Build query for wallet change logs
var query = _context.UserWalletChangeLogs var query = _context.UserWalletHistories
.AsNoTracking() .AsNoTracking()
.Where(x => x.WalletId == userWallet.Id); .Where(x => x.WalletId == userWallet.Id);
@@ -53,7 +53,7 @@ public class GetCustomerWalletChangeLogQueryHandler : IRequestHandler<GetCustome
// Order by newest first // Order by newest first
var result = await query var result = await query
.OrderByDescending(x => x.Created) .OrderByDescending(x => x.Created)
.ProjectToType<GetCustomerWalletChangeLogResponseDto>() .ProjectToType<GetCustomerWalletHistoryResponseDto>()
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return result; return result;
@@ -1,6 +1,6 @@
namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletChangeLog; namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory;
public class GetCustomerWalletChangeLogResponseDto public class GetCustomerWalletHistoryResponseDto
{ {
/// <summary> /// <summary>
/// موجودی جاری /// موجودی جاری
@@ -3,7 +3,7 @@ namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawal
public class GetCustomerWithdrawalSettingsResponseDto public class GetCustomerWithdrawalSettingsResponseDto
{ {
/// <summary> /// <summary>
/// حداقل مبلغ برداشت (ریال) /// حداقل مبلغ برداشت (تومان)
/// </summary> /// </summary>
public long MinWithdrawalAmount { get; set; } public long MinWithdrawalAmount { get; set; }
} }
@@ -1,21 +0,0 @@
using CMSMicroservice.Domain.Events;
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.CreateNewUserWalletChangeLog;
public class CreateNewUserWalletChangeLogCommandHandler : IRequestHandler<CreateNewUserWalletChangeLogCommand, CreateNewUserWalletChangeLogResponseDto>
{
private readonly IApplicationDbContext _context;
public CreateNewUserWalletChangeLogCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<CreateNewUserWalletChangeLogResponseDto> Handle(CreateNewUserWalletChangeLogCommand request,
CancellationToken cancellationToken)
{
var entity = request.Adapt<UserWalletChangeLog>();
await _context.UserWalletChangeLogs.AddAsync(entity, cancellationToken);
entity.AddDomainEvent(new CreateNewUserWalletChangeLogEvent(entity));
await _context.SaveChangesAsync(cancellationToken);
return entity.Adapt<CreateNewUserWalletChangeLogResponseDto>();
}
}
@@ -1,7 +0,0 @@
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.CreateNewUserWalletChangeLog;
public class CreateNewUserWalletChangeLogResponseDto
{
//شناسه
public long Id { get; set; }
}
@@ -1,7 +0,0 @@
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.DeleteUserWalletChangeLog;
public record DeleteUserWalletChangeLogCommand : IRequest<Unit>
{
//شناسه
public long Id { get; init; }
}
@@ -1,22 +0,0 @@
using CMSMicroservice.Domain.Events;
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.DeleteUserWalletChangeLog;
public class DeleteUserWalletChangeLogCommandHandler : IRequestHandler<DeleteUserWalletChangeLogCommand, Unit>
{
private readonly IApplicationDbContext _context;
public DeleteUserWalletChangeLogCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(DeleteUserWalletChangeLogCommand request, CancellationToken cancellationToken)
{
var entity = await _context.UserWalletChangeLogs
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(UserWalletChangeLog), request.Id);
entity.IsDeleted = true;
_context.UserWalletChangeLogs.Update(entity);
entity.AddDomainEvent(new DeleteUserWalletChangeLogEvent(entity));
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
@@ -1,16 +0,0 @@
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.DeleteUserWalletChangeLog;
public class DeleteUserWalletChangeLogCommandValidator : AbstractValidator<DeleteUserWalletChangeLogCommand>
{
public DeleteUserWalletChangeLogCommandValidator()
{
RuleFor(model => model.Id)
.NotNull();
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(ValidationContext<DeleteUserWalletChangeLogCommand>.CreateWithOptions((DeleteUserWalletChangeLogCommand)model, x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -1,22 +0,0 @@
using CMSMicroservice.Domain.Events;
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.UpdateUserWalletChangeLog;
public class UpdateUserWalletChangeLogCommandHandler : IRequestHandler<UpdateUserWalletChangeLogCommand, Unit>
{
private readonly IApplicationDbContext _context;
public UpdateUserWalletChangeLogCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(UpdateUserWalletChangeLogCommand request, CancellationToken cancellationToken)
{
var entity = await _context.UserWalletChangeLogs
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(UserWalletChangeLog), request.Id);
request.Adapt(entity);
_context.UserWalletChangeLogs.Update(entity);
entity.AddDomainEvent(new UpdateUserWalletChangeLogEvent(entity));
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
@@ -1,22 +0,0 @@
using CMSMicroservice.Domain.Events;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.EventHandlers;
public class CreateNewUserWalletChangeLogEventHandler : INotificationHandler<CreateNewUserWalletChangeLogEvent>
{
private readonly ILogger<
CreateNewUserWalletChangeLogEventHandler> _logger;
public CreateNewUserWalletChangeLogEventHandler(ILogger<CreateNewUserWalletChangeLogEventHandler> logger)
{
_logger = logger;
}
public Task Handle(CreateNewUserWalletChangeLogEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().Name);
return Task.CompletedTask;
}
}
@@ -1,22 +0,0 @@
using CMSMicroservice.Domain.Events;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.EventHandlers;
public class DeleteUserWalletChangeLogEventHandler : INotificationHandler<DeleteUserWalletChangeLogEvent>
{
private readonly ILogger<
DeleteUserWalletChangeLogEventHandler> _logger;
public DeleteUserWalletChangeLogEventHandler(ILogger<DeleteUserWalletChangeLogEventHandler> logger)
{
_logger = logger;
}
public Task Handle(DeleteUserWalletChangeLogEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().Name);
return Task.CompletedTask;
}
}
@@ -1,22 +0,0 @@
using CMSMicroservice.Domain.Events;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.EventHandlers;
public class UpdateUserWalletChangeLogEventHandler : INotificationHandler<UpdateUserWalletChangeLogEvent>
{
private readonly ILogger<
UpdateUserWalletChangeLogEventHandler> _logger;
public UpdateUserWalletChangeLogEventHandler(ILogger<UpdateUserWalletChangeLogEventHandler> logger)
{
_logger = logger;
}
public Task Handle(UpdateUserWalletChangeLogEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().Name);
return Task.CompletedTask;
}
}
@@ -1,14 +0,0 @@
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Queries.GetAllUserWalletChangeLogByFilter;
public class GetAllUserWalletChangeLogByFilterQueryValidator : AbstractValidator<GetAllUserWalletChangeLogByFilterQuery>
{
public GetAllUserWalletChangeLogByFilterQueryValidator()
{
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(ValidationContext<GetAllUserWalletChangeLogByFilterQuery>.CreateWithOptions((GetAllUserWalletChangeLogByFilterQuery)model, x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -1,7 +0,0 @@
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Queries.GetUserWalletChangeLog;
public record GetUserWalletChangeLogQuery : IRequest<GetUserWalletChangeLogResponseDto>
{
//شناسه
public long Id { get; init; }
}
@@ -1,22 +0,0 @@
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Queries.GetUserWalletChangeLog;
public class GetUserWalletChangeLogQueryHandler : IRequestHandler<GetUserWalletChangeLogQuery, GetUserWalletChangeLogResponseDto>
{
private readonly IApplicationDbContext _context;
public GetUserWalletChangeLogQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetUserWalletChangeLogResponseDto> Handle(GetUserWalletChangeLogQuery request,
CancellationToken cancellationToken)
{
var response = await _context.UserWalletChangeLogs
.AsNoTracking()
.Where(x => x.Id == request.Id)
.ProjectToType<GetUserWalletChangeLogResponseDto>()
.FirstOrDefaultAsync(cancellationToken);
return response ?? throw new NotFoundException(nameof(UserWalletChangeLog), request.Id);
}
}
@@ -1,5 +1,5 @@
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.CreateNewUserWalletChangeLog; namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.CreateNewUserWalletHistory;
public record CreateNewUserWalletChangeLogCommand : IRequest<CreateNewUserWalletChangeLogResponseDto> public record CreateNewUserWalletHistoryCommand : IRequest<CreateNewUserWalletHistoryResponseDto>
{ {
//شناسه کیف پول //شناسه کیف پول
public long WalletId { get; init; } public long WalletId { get; init; }
@@ -0,0 +1,21 @@
using CMSMicroservice.Domain.Events;
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.CreateNewUserWalletHistory;
public class CreateNewUserWalletHistoryCommandHandler : IRequestHandler<CreateNewUserWalletHistoryCommand, CreateNewUserWalletHistoryResponseDto>
{
private readonly IApplicationDbContext _context;
public CreateNewUserWalletHistoryCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<CreateNewUserWalletHistoryResponseDto> Handle(CreateNewUserWalletHistoryCommand request,
CancellationToken cancellationToken)
{
var entity = request.Adapt<UserWalletHistory>();
await _context.UserWalletHistories.AddAsync(entity, cancellationToken);
entity.AddDomainEvent(new CreateNewUserWalletHistoryEvent(entity));
await _context.SaveChangesAsync(cancellationToken);
return entity.Adapt<CreateNewUserWalletHistoryResponseDto>();
}
}
@@ -1,7 +1,7 @@
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.CreateNewUserWalletChangeLog; namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.CreateNewUserWalletHistory;
public class CreateNewUserWalletChangeLogCommandValidator : AbstractValidator<CreateNewUserWalletChangeLogCommand> public class CreateNewUserWalletHistoryCommandValidator : AbstractValidator<CreateNewUserWalletHistoryCommand>
{ {
public CreateNewUserWalletChangeLogCommandValidator() public CreateNewUserWalletHistoryCommandValidator()
{ {
RuleFor(model => model.WalletId) RuleFor(model => model.WalletId)
.NotNull(); .NotNull();
@@ -18,7 +18,7 @@ public class CreateNewUserWalletChangeLogCommandValidator : AbstractValidator<Cr
} }
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) => public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{ {
var result = await ValidateAsync(ValidationContext<CreateNewUserWalletChangeLogCommand>.CreateWithOptions((CreateNewUserWalletChangeLogCommand)model, x => x.IncludeProperties(propertyName))); var result = await ValidateAsync(ValidationContext<CreateNewUserWalletHistoryCommand>.CreateWithOptions((CreateNewUserWalletHistoryCommand)model, x => x.IncludeProperties(propertyName)));
if (result.IsValid) if (result.IsValid)
return Array.Empty<string>(); return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage); return result.Errors.Select(e => e.ErrorMessage);
@@ -0,0 +1,7 @@
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.CreateNewUserWalletHistory;
public class CreateNewUserWalletHistoryResponseDto
{
//شناسه
public long Id { get; set; }
}
@@ -0,0 +1,7 @@
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.DeleteUserWalletHistory;
public record DeleteUserWalletHistoryCommand : IRequest<Unit>
{
//شناسه
public long Id { get; init; }
}
@@ -0,0 +1,22 @@
using CMSMicroservice.Domain.Events;
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.DeleteUserWalletHistory;
public class DeleteUserWalletHistoryCommandHandler : IRequestHandler<DeleteUserWalletHistoryCommand, Unit>
{
private readonly IApplicationDbContext _context;
public DeleteUserWalletHistoryCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(DeleteUserWalletHistoryCommand request, CancellationToken cancellationToken)
{
var entity = await _context.UserWalletHistories
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(UserWalletHistory), request.Id);
entity.IsDeleted = true;
_context.UserWalletHistories.Update(entity);
entity.AddDomainEvent(new DeleteUserWalletHistoryEvent(entity));
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
@@ -0,0 +1,16 @@
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.DeleteUserWalletHistory;
public class DeleteUserWalletHistoryCommandValidator : AbstractValidator<DeleteUserWalletHistoryCommand>
{
public DeleteUserWalletHistoryCommandValidator()
{
RuleFor(model => model.Id)
.NotNull();
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(ValidationContext<DeleteUserWalletHistoryCommand>.CreateWithOptions((DeleteUserWalletHistoryCommand)model, x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -1,5 +1,5 @@
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.UpdateUserWalletChangeLog; namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.UpdateUserWalletHistory;
public record UpdateUserWalletChangeLogCommand : IRequest<Unit> public record UpdateUserWalletHistoryCommand : IRequest<Unit>
{ {
//شناسه //شناسه
public long Id { get; init; } public long Id { get; init; }
@@ -0,0 +1,22 @@
using CMSMicroservice.Domain.Events;
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.UpdateUserWalletHistory;
public class UpdateUserWalletHistoryCommandHandler : IRequestHandler<UpdateUserWalletHistoryCommand, Unit>
{
private readonly IApplicationDbContext _context;
public UpdateUserWalletHistoryCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(UpdateUserWalletHistoryCommand request, CancellationToken cancellationToken)
{
var entity = await _context.UserWalletHistories
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(UserWalletHistory), request.Id);
request.Adapt(entity);
_context.UserWalletHistories.Update(entity);
entity.AddDomainEvent(new UpdateUserWalletHistoryEvent(entity));
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}

Some files were not shown because too many files have changed in this diff Show More