Extract VerifyUserPackagePurchasePaymentCommand with idempotency, DB lock,
and IUserPaymentLock so ZarinpalReconciliationJob cannot double-credit wallets.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add idempotent verify with serializable transaction and wallet-history check
to stop concurrent callback/race from charging the wallet twice.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Introduced GetUnverifiedAuthoritiesAsync method to retrieve authorities with unverified payments.
- Implemented ZarinPal-specific logic for fetching unverified authorities from the new endpoint.
- Added background job for Zarinpal payment reconciliation every 30 minutes.
Co-authored-by: Cursor <cursoragent@cursor.com>
- commission.proto: add BulkCreditPayouts rpc + request/response messages; bump version to 0.0.198
- BulkCreditPayoutsCommand + Handler: load all Pending payouts for given week, credit UserWallet.Balance for each user, create UserWalletHistory + CommissionPayoutHistory records, set Status=Paid + PaidAt
- CommissionService: wire up BulkCreditPayouts gRPC method
- CommissionProfile: add Mapster mappings for request/result ↔ proto types
Previously ProcessUserPayouts created records with Status=Pending but never credited wallets, making CustomerWithdrawBalance unreachable (it requires Status==Paid).
Co-authored-by: Cursor <cursoragent@cursor.com>
- UserOrderService.SubmitShopBuyOrder: inject IInventoryService; validate
available stock per item before any financial operation; call ConfirmSaleAsync
and increment Product.SaleCount after order is persisted.
- CancelOrderByAdminCommandHandler: inject IInventoryService; include
FactorDetails in query; call ProcessReturnAsync for each item so stock
is restored when an order is cancelled by admin.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Adjust MagicWalletEntryThreshold from 1,000,000 to 100,000
- Update MagicWalletDefaultMaxDeposit from 1,000,000,000 to 100,000,000
- Change MagicWalletDefaultMaxCredit from 2,500,000,000 to 250,000,000
- Modify comments in UserOrderService to reflect updated threshold values
- Add sort_by (StringValue) to GetDiscountProductsRequest proto to enable dynamic sorting
- Add sale_count (int32) to DiscountProductDto proto for top-seller exposure
- Update GetDiscountProductsQuery with SortBy property
- Update GetDiscountProductsQueryHandler to use ApplyOrder(SortBy) with fallback to Created desc; project SaleCount in SELECT
- Add SortBy and SaleCount Mapster mappings in DiscountProductProfile
- Bump CMSMicroservice.Protobuf version to 0.0.196
Co-authored-by: Cursor <cursoragent@cursor.com>
- Updated AcceptClubMembershipContractCommandHandler to set PackagePurchasedAt to the contract-signing time, ensuring accurate pool charging.
- Modified ActivateClubMembershipCommand to allow specifying a PackageId for forced activation, defaulting to the base package if not provided.
- Improved ActivateClubMembershipCommandHandler to handle new and existing memberships differently regarding pool charging.
- Added validation logic in GetWeeklyCommissionPoolQueryHandler to compare TotalPoolAmount against actual activations.
- Introduced validation fields in WeeklyCommissionPoolDto for tracking discrepancies and expected amounts.
- Updated stored procedures to ensure accurate calculations based on package purchases and to handle empty pools.
- Enhanced protobuf definitions to include validation fields for commission pools and network tree nodes.
- Added mapping for new fields in CommissionProfile and NetworkMembershipProfile.
- Created DiscountProductProfile for mapping discount product requests and responses.
- SP sp_CalculateWeeklyBalances: added @PackageId, @InputMaxBalancesPerLeg, @InputMaxNetworkLevel params; filters by PackageId
- SP sp_CalculateWeeklyCommissionPool: added @PackageId param; all queries filter by PackageId/WeeklyPoolId for isolation
- ICommissionCalculationStrategy: added optional PackageId param to both methods
- StoredProcedureCommissionCalculationStrategy: loops per-package for both Balance and Pool methods; filters by packageId if provided
- OrmCommissionCalculationStrategy: signature updated to match interface
- TriggerWeeklyCalculationCommand: added PackageId optional field
- TriggerWeeklyCalculationCommandHandler: passes PackageId to strategy
- GetWeeklyCommissionPoolQuery: added optional PackageId filter
- GetWeeklyCommissionPoolQueryHandler: filters pool by PackageId if provided
- GetAllWeeklyPoolsQuery/Handler/DTO: added PackageId filter + PackageTitle in response
- Proto commission.proto: added package_id to TriggerWeeklyCalculationRequest, GetWeeklyCommissionPoolRequest, WeeklyCommissionPoolModel, GetWeeklyCommissionPoolResponse, GetAllWeeklyPoolsRequest
- CommissionProfile: added explicit mappings for TriggerWeeklyCalculation, GetWeeklyCommissionPool, GetAllWeeklyPools
Fixes: SP picks wrong pool when multiple packages per week
Fixes: SP ignores PackageId on ForceRecalculate (now uses WeeklyPoolId)
Fixes: Zero-balance pools not fully recorded (now sets TotalBalances=0, ValuePerBalance=0)
- 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.
- 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.
- 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).
- 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 (تأیید تراکنش ناموفق).
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
- 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
- CustomerPurchasePackage: append orderId to callback URL before sending to gateway
- Ensures PaymentCallback page receives orderId via query string for verification
- 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
- 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)
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).
- userwallet.proto: added string user_name = 5 to GetAllUserWalletByFilterResponseModel
- UserWalletService: enriches response with user names from Users table
- Proto version bumped to 0.0.183
- ActivateClubMembershipCommandHandler: block activation in Magic mode
- CheckAndProcessDayaLoansCommandHandler: reject Daya after first cycle
- userwallet.proto: add purchase_cycle_count to GetMagicWalletStatusResponse
- UserWalletService: query ClubMembershipCycles count for cycle info
- EF Migration u21: ClubMembershipCycle table + UserWallet Magic fields