From f968a6c005cb00869cb46801d16bdd838513598e Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Wed, 18 Feb 2026 21:44:39 +0330 Subject: [PATCH 1/4] docs: move README to totalDoc, replace with placeholder --- README.md | 446 +----------------- .../appsettings.Development.json | 84 ---- 2 files changed, 2 insertions(+), 528 deletions(-) delete mode 100644 src/CMSMicroservice.WebApi/appsettings.Development.json diff --git a/README.md b/README.md index 829ad70..f542b5b 100644 --- a/README.md +++ b/README.md @@ -1,445 +1,3 @@ -# CMS Microservice - Network & Club Commission + Inventory Management System +# CMS Microservice -[![Status](https://img.shields.io/badge/Status-Active%20Development-success)]() -[![Progress](https://img.shields.io/badge/Inventory%20System-Phase%202%20Complete-blue)]() -[![Phase](https://img.shields.io/badge/Next-Business%20Services-orange)]() - -## 📊 Project Status (January 2026) - -### 🏪 Inventory Management System - NEW! -**Progress**: Phase 2 Complete (50%) -**Architecture**: Clean Architecture + CQRS + Repository Pattern - -#### ✅ Completed Phases -1. ✅ **Phase 1: Infrastructure & Domain Layer** - - Domain Entities: `InventoryItem`, `StockMovement`, `Warehouse` - - Domain Enums: `StockMovementType` - - EF Core Configurations with proper indexing - - Database migration applied - -2. ✅ **Phase 2: Repository Pattern & CQRS** - - Repository Interfaces & Implementations - - CQRS Commands (17 commands) - - CQRS Queries (35 queries) - - MediatR Handlers (52 handlers) - -#### 🔄 In Progress -3. 🔄 **Phase 3: Business Services Layer** -4. ⏳ **Phase 4: DTOs & AutoMapper** -5. ⏳ **Phase 5: API Controllers** - ---- - -### 💼 Commission System - Production Ready -**Progress**: 85% Complete -**MVP Status**: ✅ 100% Complete - -#### ✅ Completed Features -- ✅ Binary network tree with automatic placement -- ✅ Club membership (Member/Trial) with commission rates -- ✅ Weekly commission calculation (Lesser Leg algorithm) -- ✅ Background worker with Hangfire -- ✅ Email + SMS notifications (MailKit + Kavenegar) -- ✅ Health check endpoints (Kubernetes-ready) - -### 🟡 Partially Complete -- Phase 10: Withdrawal & Settlement (40%) - - ✅ Commands & Database - - ❌ Payment Gateway Integration - -### ❌ Not Started -- Phase 9: Club Shop & Product Integration (0%) - ---- - -## 🚀 Recent Updates (January 2026) - -### 🏪 Inventory Management System - NEW! ✅ -**Complete CQRS-based inventory management with:** - -#### Domain Layer: -- ✅ `InventoryItem` - Multi-warehouse product tracking with min/max thresholds -- ✅ `StockMovement` - Complete audit trail with 8 movement types -- ✅ `Warehouse` - Multi-location support with default warehouse - -#### Repository Pattern: -- ✅ `IInventoryItemRepository` - 25+ methods for inventory operations -- ✅ `IStockMovementRepository` - Movement tracking & analytics -- ✅ `IWarehouseRepository` - Warehouse management & statistics - -#### CQRS Commands (17 total): -- **Inventory:** Create, Update, Delete, Reserve, Release, Reduce, Increase -- **Movement:** Create, BulkCreate, Delete -- **Warehouse:** Create, Update, Delete, SetDefault, Activate, BulkCreate - -#### CQRS Queries (35 total): -- **Inventory:** GetById, Search, LowStock, OutOfStock, CheckAvailability -- **Movement:** GetHistory, GetByOrder, Search, Analytics, DailyVolume, TopMoving -- **Warehouse:** GetById, Search, GetStats, GetLowStock, GetAllStats - -#### Business Features: -- ✅ Multi-warehouse inventory management -- ✅ Stock reservation system for orders -- ✅ Automatic movement tracking -- ✅ Low stock & out-of-stock alerts -- ✅ Advanced analytics & reporting -- ✅ Bulk operations support -- ✅ Transaction-safe operations - ---- - -### Email & SMS Notifications - COMPLETED ✅ -- ✅ **MailKit 4.14.1** for Email (SMTP with HTML templates) -- ✅ **Kavenegar 1.2.5** for SMS (Iranian SMS gateway) -- ✅ User.Email field added with migration -- ✅ 3 notification types: Commission, Club activation, Errors -- ✅ Persian RTL templates with rich formatting -- ✅ Production configuration guide created - -### Hangfire Job Scheduling - COMPLETED ✅ -- ✅ Dashboard UI at `/hangfire` -- ✅ Cron schedule: Sunday 00:05 UTC -- ✅ SQL Server persistence -- ✅ Manual trigger API endpoints -- ✅ Distributed execution support - -### Infrastructure Enhancements - COMPLETED ✅ -- ✅ Health Check endpoints (`/health`, `/health/ready`, `/health/live`) -- ✅ AlertService (structured logging for Sentry/Slack) -- ✅ Retry logic (Polly 8.5.0 with exponential backoff) -- ✅ WorkerExecutionLog (database audit trail) -- ✅ CurrentUserService (JWT authentication context) - ---- - -## 🏗️ Architecture - -**Clean Architecture** with 4 layers: -``` -CMSMicroservice.Domain/ # Entities, Enums, Interfaces -├── Entities/ -│ ├── InventoryItem.cs # NEW: Inventory tracking -│ ├── StockMovement.cs # NEW: Movement audit -│ └── Warehouse.cs # NEW: Multi-warehouse -├── Enums/ -│ └── StockMovementType.cs # NEW: Movement types - -CMSMicroservice.Application/ # CQRS (Commands, Queries, MediatR) -├── Features/ -│ ├── InventoryItems/ # NEW: Inventory CQRS -│ │ ├── Commands/ -│ │ ├── Queries/ -│ │ └── Handlers/ -│ ├── StockMovements/ # NEW: Movement CQRS -│ │ ├── Commands/ -│ │ ├── Queries/ -│ │ └── Handlers/ -│ └── Warehouses/ # NEW: Warehouse CQRS -│ ├── Commands/ -│ ├── Queries/ -│ └── Handlers/ -└── Common/Interfaces/ - └── Repositories/ # NEW: Repository interfaces - -CMSMicroservice.Infrastructure/ # DbContext, Services, Background Jobs -├── Persistence/ -│ ├── Context/ -│ ├── Configurations/ # NEW: EF Core configs -│ ├── Repositories/ # NEW: Repository implementations -│ └── Migrations/ -└── DependencyInjection.cs # NEW: DI setup - -CMSMicroservice.WebApi/ # gRPC Services, Controllers -CMSMicroservice.Protobuf/ # Protocol Buffers definitions -``` - -**Technology Stack**: -- .NET 9.0 -- Entity Framework Core 9.0.11 -- gRPC + JSON Transcoding -- Hangfire 1.8.22 (Job Scheduling) -- MediatR 13.0.0 (CQRS) -- Polly 8.5.0 (Resilience) -- MailKit 4.14.1 (Email) -- Kavenegar 1.2.5 (SMS) -- SQL Server - ---- - -## 📖 Documentation - -- **[Development Plan](docs/development-plan.md)** - NEW: Inventory system roadmap -- **[Implementation Progress](docs/implementation-progress.md)** - Detailed phase-by-phase progress -- **[Email/SMS Configuration Guide](docs/email-sms-configuration-guide.md)** - Production setup instructions -- **[Balance Calculation Logic](docs/balance-calculation-carryover-logic.md)** - Commission algorithm details -- **[Binary Tree Registration](docs/binary-tree-registration-guide.md)** - Network tree guide -- **[Network Club Commission System](docs/network-club-commission-system-v1.1.md)** - Full system specification - ---- - -## 🏪 Inventory System Usage - -### Create Warehouse -```csharp -await mediator.Send(new CreateWarehouseCommand -{ - Name = "Main Warehouse", - Code = "WH-001", - IsDefault = true, - IsActive = true -}); -``` - -### Create Inventory Item -```csharp -await mediator.Send(new CreateInventoryItemCommand -{ - ProductId = 1, - WarehouseId = 1, - Quantity = 100, - MinQuantity = 10, - MaxQuantity = 1000 -}); -``` - -### Reserve Stock for Order -```csharp -await mediator.Send(new ReserveInventoryCommand -{ - Id = inventoryId, - Quantity = 5, - OrderId = 12345 -}); -``` - -### Check Availability -```csharp -bool available = await mediator.Send( - new CheckInventoryAvailabilityQuery(inventoryId, 10)); -``` - -### Get Low Stock Alerts -```csharp -var lowStock = await mediator.Send(new GetLowStockItemsQuery -{ - WarehouseId = 1, - Count = 50 -}); -``` - -### Get Movement Analytics -```csharp -var summary = await mediator.Send(new GetMovementSummaryQuery -{ - FromDate = DateTime.Now.AddDays(-7), - ToDate = DateTime.Now -}); - -var topProducts = await mediator.Send(new GetTopMovingProductsQuery -{ - FromDate = DateTime.Now.AddDays(-30), - ToDate = DateTime.Now, - Count = 10 -}); -``` - ---- - -## 🚀 Quick Start - -### Prerequisites -- .NET 9.0 SDK -- SQL Server (local or remote) -- (Optional) Gmail account for Email -- (Optional) Kavenegar account for SMS - -### 1. Clone & Build -```bash -cd /home/masoud/Apps/project/FourSat/CMS/src -dotnet build -``` - -### 2. Configure Database -Update `appsettings.json` with your SQL Server connection: -```json -"ConnectionStrings": { - "DefaultConnection": "Server=YOUR_SERVER;Database=Foursat_CMS;..." -} -``` - -### 3. Apply Migrations -```bash -cd CMSMicroservice.WebApi -dotnet ef database update -``` - -### 4. Configure Notifications (Optional) -See [Email/SMS Configuration Guide](docs/email-sms-configuration-guide.md) - -### 5. Run -```bash -dotnet run --urls="http://localhost:5133" -``` - -### 6. Access Endpoints -- **Health**: http://localhost:5133/health -- **Hangfire Dashboard**: http://localhost:5133/hangfire -- **gRPC**: localhost:5133 (HTTP/2) - ---- - -## 🔧 Configuration - -### Email (SMTP) -```json -"Email": { - "Enabled": true, - "SmtpHost": "smtp.gmail.com", - "SmtpPort": 587, - "SmtpUsername": "your-email@gmail.com", - "SmtpPassword": "your-gmail-app-password", - "FromEmail": "noreply@foursat.com", - "FromName": "FourSat CMS", - "EnableSsl": true -} -``` - -### SMS (Kavenegar) -```json -"Sms": { - "Enabled": true, - "Provider": "Kavenegar", - "KavenegarApiKey": "YOUR_API_KEY", - "Sender": "10008663" -} -``` - -### Background Worker -```csharp -// Cron: "5 0 * * 0" = Every Sunday at 00:05 UTC -RecurringJob.AddOrUpdate( - "weekly-commission-calculation", - job => job.ExecuteAsync(CancellationToken.None), - "5 0 * * 0"); -``` - ---- - -## 🧪 Testing - -### Manual Trigger (via API) -```bash -# Trigger weekly calculation immediately -curl -X POST http://localhost:5133/api/admin/trigger-weekly-calculation - -# Trigger recurring job now -curl -X POST http://localhost:5133/api/admin/trigger-recurring-job-now - -# Get recurring jobs status -curl http://localhost:5133/api/admin/recurring-jobs-status -``` - -### Health Checks -```bash -curl http://localhost:5133/health # Overall health -curl http://localhost:5133/health/ready # Readiness probe (K8s) -curl http://localhost:5133/health/live # Liveness probe (K8s) -``` - ---- - -## 📊 What's Remaining? - -### 🏪 Inventory System (Current Focus) -1. **Phase 3: Business Services** (In Progress) - - `IInventoryManagementService` - High-level operations - - `IStockMovementService` - Movement orchestration - - `IWarehouseService` - Warehouse business logic - - `IInventoryReportingService` - Advanced reporting - -2. **Phase 4: DTOs & AutoMapper** (Next) - - Request/Response DTOs - - AutoMapper profiles - - Validation rules - -3. **Phase 5: API Controllers** (Planned) - - `InventoryController` - REST API - - `WarehouseController` - Warehouse management - - `StockMovementController` - Movement tracking - - Swagger documentation - -### 💼 Commission System -1. **Payment Gateway Integration** (Phase 10 - 1 week) - - Daya or Bank Mellat API integration - - IBAN transfer automation - - Admin approval UI in BackOffice - -2. **Production Configuration** (30 minutes) - - Gmail App Password setup - - Kavenegar API key registration - - Update `appsettings.Production.json` - -### Medium Priority -3. **Club Shop Integration** (Phase 9 - 2 weeks) - - Product catalog for club memberships - - Shopping cart integration - - Auto-activation on purchase - -### Low Priority -4. **Testing** (Phase 7 - Postponed) - - Unit tests for business logic - - Integration tests for API - - Load testing for background worker - -### Optional Enhancements -- Redis distributed locks (multi-server deployment) -- Sentry error tracking (API key needed) -- Slack notifications (webhook needed) -- FCM push notifications - ---- - -## 🎯 MVP Features (100% Complete) - -### 💼 Commission System: -✅ Binary network tree with automatic placement -✅ Club membership (Member/Trial) with different commission rates -✅ Weekly commission calculation (Lesser Leg algorithm) -✅ Background worker with Hangfire (cron scheduling) -✅ Balance carryover logic (rollover unused volumes) -✅ MaxWeeklyBalances cap enforcement -✅ Health check endpoints (Kubernetes-ready) -✅ Manual trigger API (admin control) -✅ Email + SMS notifications (MailKit + Kavenegar) -✅ Retry logic with exponential backoff (Polly) -✅ Audit trail (WorkerExecutionLog, History tables) -✅ Structured logging (AlertService for Sentry/Slack) -✅ JWT authentication context (CurrentUserService) - -### 🏪 Inventory System (Phase 2 Complete): -✅ Domain entities (InventoryItem, StockMovement, Warehouse) -✅ Multi-warehouse inventory management -✅ Stock reservation system for orders -✅ 8 movement types with complete audit trail -✅ Repository pattern with 25+ methods per repository -✅ CQRS with 17 commands and 35 queries -✅ 52 MediatR handlers with business logic -✅ Low stock and out-of-stock alerts -✅ Advanced analytics (top products, daily volume) -✅ Bulk operations support -✅ Transaction-safe operations with rollback -✅ DI container configuration - ---- - -## 👥 Team - -**Development**: FourSat Team -**Last Updated**: January 2026 - ---- - -## 📝 License - -Proprietary - FourSat Company -# Multi-remote push enabled +Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md). diff --git a/src/CMSMicroservice.WebApi/appsettings.Development.json b/src/CMSMicroservice.WebApi/appsettings.Development.json deleted file mode 100644 index 5e17cea..0000000 --- a/src/CMSMicroservice.WebApi/appsettings.Development.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "PaymentProvider": "zarinpal", - "ZarinPal": { - "MerchantId": "00000000-0000-0000-0000-000000000000", - "UseSandbox": true - }, - "CmsBaseUrl": "http://localhost:32847", - "FrontOfficeBaseUrl": "http://localhost:5268", - "JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=", - "JwtIssuer": "https://localhost", - "JwtAudience": "https://localhost", - "JwtExpiryInDays": 5, - "ConnectionStrings": { - "DefaultConnection": "Data Source=194.5.195.53,31433; Initial Catalog=Foursat;User ID=sa;Password=87zH26nbqT;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False", - "providerName": "System.Data.SqlClient" - }, - "Otp": { - "Secret": "K2w8k1h1mH2Qz1kqWk0c8kQ2Pq8q9H1eE2nqN1qQ8x7M=" - }, - "Monitoring": { - "SentryEnabled": false, - "SentryDsn": "", - "SlackEnabled": false, - "SlackWebhookUrl": "", - "EmailAlertsEnabled": false, - "AdminEmails": [ - "admin@example.com" - ], - "SmsNotificationsEnabled": false, - "SmsApiKey": "", - "SmsGatewayUrl": "" - }, - "Email": { - "Enabled": true, - "SmtpHost": "smtp.gmail.com", - "SmtpPort": 587, - "SmtpUsername": "your-email@gmail.com", - "SmtpPassword": "your-app-password", - "FromEmail": "noreply@foursat.com", - "FromName": "FourSat CMS", - "EnableSsl": true - }, - "Sms": { - "Enabled": true, - "Provider": "Kavenegar", - "KavenegarApiKey": "497263626F32626A48685A6137524C4F78575A766E4C74694A556B79317648424964655030682B554545413D", - "Sender": "1000001110100" - }, - "DayaPayment": { - "BaseUrl": "https://api.daya.ir", - "ApiKey": "YOUR_DAYA_API_KEY" - }, - "DayaApi": { - "UseMock": false, - "BaseAddress": "https://Dayadiamond.ir", - "MerchantPermissionKey": "56146364$04sXjethI5WxhItR1Q9xnmFdJzl2BB8Bclsq8dAy7YVSZp3vtt-wP7ivrcCvmKLq", - "CacheDurationMinutes": 20 - }, - "Chatika": { - "Enabled": true, - "BaseUrl": "https://api.chatika.ir", - "ApiKey": "tIukvL8dnV4cB3yVWcCD9Xyfbj8rBxm5wPt2mLyJCgTsBBoMTWjt6mFEqQwpw-er" - }, - "BackgroundJobs": { - "WeeklyCommissionCalculation": { - "Enabled": true, - "CronExpression": "5 0 * * 0" - } - }, - "AllowedHosts": "*", - "Kestrel": { - "EndpointDefaults": { - "Protocols": "Http2" - } - }, - "Authentication": { - "Authority": "https://ids.domain.com/", - "Audience": "domain_api" - }, - "Seq": { - "ServerUrl": "https://seq.afrino.co", - "ApiKey": "oxpvpUzU1pZxMS4s3Fqq" - } -} From 04e8c49fa7ede9a997db5724971bbd84f896864f Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Wed, 18 Feb 2026 23:12:03 +0330 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20IPG=20DiscountBalance=20should=20be?= =?UTF-8?q?=202=C3=97=20BasePackageAmount=20(112M=20not=2056M)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both VerifyBasePackagePayment and VerifyPackagePurchase handlers were charging DiscountBalance with 1× amount instead of 2×. Now consistent with DayaLoan and ManualPayment handlers. - VerifyBasePackagePaymentCommandHandler: DiscountBalance += BasePackageAmount * 2 - VerifyPackagePurchaseCommandHandler: DiscountBalance += order.Amount * 2 - Change logs updated to reflect correct 2× discount value --- .../VerifyBasePackagePaymentCommandHandler.cs | 4 ++-- .../VerifyPackagePurchaseCommandHandler.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandHandler.cs b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandHandler.cs index df174cc..b7d79c3 100644 --- a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandHandler.cs +++ b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandHandler.cs @@ -133,7 +133,7 @@ public class VerifyBasePackagePaymentCommandHandler : IRequestHandler {NewBalance}, DiscountBalance: {OldDiscount} -> {NewDiscount}", @@ -157,7 +157,7 @@ public class VerifyBasePackagePaymentCommandHandler : IRequestHandler {NewBalance}", @@ -132,7 +132,7 @@ public class VerifyPackagePurchaseCommandHandler ChangeValue = order.Amount, CurrentNetworkBalance = wallet.NetworkBalance, ChangeNerworkValue = 0, - CurrentDiscountBalance = wallet.DiscountBalance - order.Amount, // قبل از شارژ DiscountBalance + CurrentDiscountBalance = wallet.DiscountBalance - (order.Amount * 2), // قبل از شارژ DiscountBalance ChangeDiscountValue = 0, IsIncrease = true, RefrenceId = transaction.Id @@ -148,7 +148,7 @@ public class VerifyPackagePurchaseCommandHandler CurrentNetworkBalance = wallet.NetworkBalance, ChangeNerworkValue = 0, CurrentDiscountBalance = wallet.DiscountBalance, - ChangeDiscountValue = order.Amount, + ChangeDiscountValue = order.Amount * 2, IsIncrease = true, RefrenceId = transaction.Id }; From 2a569a024fc4574c826a208a30172d3588c6359c Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sat, 21 Feb 2026 19:50:28 +0330 Subject: [PATCH 3/4] feat: Implement Magic Wallet functionality - Added ClubMembershipCycle entity and DbSet to IApplicationDbContext. - Updated VAT rate from 9% to 10% in VatCalculator and related areas. - Introduced Magic Wallet settings in SystemConstants. - Enhanced UserWallet entity to support Magic Wallet features. - Updated TransactionType enum to include Magic Wallet transactions. - Configured UserWallet to handle Magic Wallet properties in ApplicationDbContext. - Implemented OrmCommissionCalculationStrategy to exclude Magic Wallet users from commission calculations. - Added Protobuf definitions for Magic Wallet methods and responses. - Created PaymentCallbackController endpoint for handling Magic Wallet charge callbacks. - Updated UserOrderService to manage Magic Wallet state transitions. - Developed UserWalletService to support Magic Wallet operations. - Created ChargeMagicWalletCommand and its handler for initiating Magic Wallet charges. - Implemented VerifyMagicWalletChargeCommand and handler for payment verification. - Added validation for ChargeMagicWalletCommand. - Established ClubMembershipCycle configuration for EF Core. - Introduced WalletMode enum to differentiate between Normal and Magic modes. --- .../ActivateClubMembershipCommandHandler.cs | 38 +++- .../CalculateWeeklyBalancesCommandHandler.cs | 17 +- .../Interfaces/IApplicationDbContext.cs | 1 + .../Common/Services/VatCalculator.cs | 8 +- .../ChargeMagicWalletCommand.cs | 21 ++ .../ChargeMagicWalletCommandHandler.cs | 174 +++++++++++++++ .../ChargeMagicWalletCommandValidator.cs | 19 ++ .../VerifyMagicWalletChargeCommand.cs | 19 ++ .../VerifyMagicWalletChargeCommandHandler.cs | 209 ++++++++++++++++++ .../Common/SystemConstants.cs | 29 +++ .../Entities/Club/ClubMembership.cs | 5 + .../Entities/Club/ClubMembershipCycle.cs | 64 ++++++ .../Entities/UserWallet.cs | 31 ++- .../Enums/TransactionType.cs | 10 + .../Enums/WalletMode.cs | 13 ++ .../Persistence/ApplicationDbContext.cs | 1 + .../ClubMembershipCycleConfiguration.cs | 45 ++++ .../Configurations/UserWalletConfiguration.cs | 14 ++ .../OrmCommissionCalculationStrategy.cs | 15 +- .../Protos/userwallet.proto | 40 ++++ .../Controllers/PaymentCallbackController.cs | 43 ++++ .../Services/UserOrderService.cs | 59 ++++- .../Services/UserWalletService.cs | 61 ++++- 23 files changed, 914 insertions(+), 22 deletions(-) create mode 100644 src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommand.cs create mode 100644 src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandHandler.cs create mode 100644 src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandValidator.cs create mode 100644 src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommand.cs create mode 100644 src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommandHandler.cs create mode 100644 src/CMSMicroservice.Domain/Entities/Club/ClubMembershipCycle.cs create mode 100644 src/CMSMicroservice.Domain/Enums/WalletMode.cs create mode 100644 src/CMSMicroservice.Infrastructure/Persistence/Configurations/ClubMembershipCycleConfiguration.cs diff --git a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs index d890d98..5847434 100644 --- a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs +++ b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs @@ -183,10 +183,9 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler c.UserId == user.Id && c.IsCurrentCycle) + .ToListAsync(cancellationToken); + + foreach (var prevCycle in previousCycles) + { + prevCycle.IsCurrentCycle = false; + } + + var maxCycleNumber = await _context.ClubMembershipCycles + .Where(c => c.ClubMembershipId == entity.Id) + .MaxAsync(c => (int?)c.CycleNumber, cancellationToken) ?? 0; + + var newCycle = new ClubMembershipCycle + { + UserId = user.Id, + ClubMembershipId = entity.Id, + CycleNumber = maxCycleNumber + 1, + PackagePurchasedAt = activationDate, + PurchaseMethod = user.PackagePurchaseMethod, + PackageAmount = SystemConstants.BasePackageAmount, + IsCurrentCycle = true + }; + + _context.ClubMembershipCycles.Add(newCycle); + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Created ClubMembershipCycle #{CycleNumber} for UserId {UserId}, MembershipId {MembershipId}", + newCycle.CycleNumber, + user.Id, + entity.Id + ); + // 7. ثبت تاریخچه var history = new ClubMembershipHistory { diff --git a/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs b/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs index ac83d4d..b7b2a0a 100644 --- a/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs +++ b/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs @@ -43,8 +43,14 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler w.WalletMode == WalletMode.Magic) + .Select(w => w.UserId) + .ToHashSetAsync(cancellationToken); + var activeClubMemberUserIds = await _context.ClubMemberships - .Where(c => c.IsActive) + .Where(c => c.IsActive && !magicModeUserIds.Contains(c.UserId)) .Select(c => c.UserId) .ToHashSetAsync(cancellationToken); @@ -256,11 +262,12 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler x.UserId == child.Id && x.IsActive, cancellationToken); + // اگر فرزند در این هفته پکیج خریده (Cycle جدید ساخته شده)، 1 امتیاز + // ⚠️ از ClubMembershipCycle.PackagePurchasedAt استفاده میکنیم (نه ActivatedAt که overwrite میشد) + var currentCycle = await _context.ClubMembershipCycles + .FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsCurrentCycle, cancellationToken); - if (membership?.ActivatedAt >= startDate && membership?.ActivatedAt <= endDate) + if (currentCycle?.PackagePurchasedAt >= startDate && currentCycle?.PackagePurchasedAt <= endDate) { count = 1; } diff --git a/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs b/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs index 8dccd92..64dcc7d 100644 --- a/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs @@ -38,6 +38,7 @@ public interface IApplicationDbContext DbSet PublicMessages { get; } DbSet ClubMemberships { get; } DbSet ClubMembershipHistories { get; } + DbSet ClubMembershipCycles { get; } DbSet ClubFeatures { get; } DbSet UserClubFeatures { get; } DbSet NetworkWeeklyBalances { get; } diff --git a/src/CMSMicroservice.Application/Common/Services/VatCalculator.cs b/src/CMSMicroservice.Application/Common/Services/VatCalculator.cs index 92e2cc9..e0d5a2e 100644 --- a/src/CMSMicroservice.Application/Common/Services/VatCalculator.cs +++ b/src/CMSMicroservice.Application/Common/Services/VatCalculator.cs @@ -6,14 +6,14 @@ namespace CMSMicroservice.Application.Common.Services; public static class VatCalculator { /// - /// نرخ VAT ایران - 9 درصد + /// نرخ VAT ایران - 10 درصد /// - public const decimal VAT_RATE = 0.09m; + public const decimal VAT_RATE = 0.10m; /// - /// نرخ VAT به صورت درصد (9) + /// نرخ VAT به صورت درصد (10) /// - public const int VAT_PERCENT = 9; + public const int VAT_PERCENT = 10; /// /// محاسبه VAT از مبلغ خالص diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommand.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommand.cs new file mode 100644 index 0000000..3e05022 --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommand.cs @@ -0,0 +1,21 @@ +using CMSMicroservice.Application.Common.Interfaces; +using MediatR; + +namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet; + +/// +/// شروع شارژ کیف‌پول جادویی از طریق درگاه پرداخت +/// مبلغ واریزی × 2.5 به Balance اعتبار داده می‌شود +/// +public class ChargeMagicWalletCommand : IRequest +{ + /// + /// شناسه کاربر + /// + public long UserId { get; set; } + + /// + /// مبلغ واریزی واقعی (ریال) — اعتبار = مبلغ × 2.5 + /// + public long Amount { get; set; } +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandHandler.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandHandler.cs new file mode 100644 index 0000000..7c947bd --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandHandler.cs @@ -0,0 +1,174 @@ +using CMSMicroservice.Application.Common.Exceptions; +using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Common; +using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Entities.Payment; +using CMSMicroservice.Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet; + +public class ChargeMagicWalletCommandHandler + : IRequestHandler +{ + private readonly IApplicationDbContext _context; + private readonly IPaymentGatewayService _paymentGateway; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public ChargeMagicWalletCommandHandler( + IApplicationDbContext context, + IPaymentGatewayService paymentGateway, + IConfiguration configuration, + ILogger logger) + { + _context = context; + _paymentGateway = paymentGateway; + _configuration = configuration; + _logger = logger; + } + + public async Task Handle( + ChargeMagicWalletCommand request, + CancellationToken cancellationToken) + { + try + { + _logger.LogInformation( + "Initiating magic wallet charge for UserId: {UserId}, Amount: {Amount}", + request.UserId, + request.Amount + ); + + // 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. بررسی وجود کیف‌پول + var wallet = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == request.UserId, cancellationToken); + + if (wallet == null) + { + _logger.LogError("Wallet not found for UserId: {UserId}", request.UserId); + throw new NotFoundException("کیف پول کاربر یافت نشد"); + } + + // 3. بررسی حالت جادویی + if (wallet.WalletMode != WalletMode.Magic) + { + _logger.LogWarning( + "User {UserId} wallet is not in Magic mode (current: {Mode})", + request.UserId, + wallet.WalletMode + ); + throw new BadRequestException( + "شارژ جادویی فقط در حالت کیف‌پول جادویی امکان‌پذیر است" + ); + } + + // 4. بررسی سقف واریزی (per-cycle) + var remainingDeposit = SystemConstants.MagicWalletMaxDeposit - wallet.MagicTotalDeposited; + + if (remainingDeposit <= 0) + { + _logger.LogWarning( + "User {UserId} has reached magic deposit cap. TotalDeposited: {TotalDeposited}", + request.UserId, + wallet.MagicTotalDeposited + ); + throw new BadRequestException( + "سقف شارژ جادویی در این دور پر شده است" + ); + } + + if (request.Amount > remainingDeposit) + { + _logger.LogWarning( + "User {UserId} amount {Amount} exceeds remaining deposit cap {Remaining}", + request.UserId, + request.Amount, + remainingDeposit + ); + throw new BadRequestException( + $"مبلغ واریزی بیش از سقف باقیمانده است. حداکثر مبلغ قابل واریز: {remainingDeposit:N0} ریال" + ); + } + + // 5. ایجاد درخواست پرداخت + var cmsBaseUrl = _configuration["CmsBaseUrl"] ?? "https://localhost:32846"; + var callbackUrl = $"{cmsBaseUrl}/api/wallet/verify-magic-charge"; + + var paymentRequest = new PaymentRequest + { + Amount = request.Amount, + UserId = user.Id, + Mobile = user.Mobile ?? "", + CallbackUrl = callbackUrl, + Description = $"شارژ کیف‌پول جادویی - کاربر {user.Id}" + }; + + var paymentResult = await _paymentGateway.InitiatePaymentAsync(paymentRequest); + + if (!paymentResult.IsSuccess) + { + _logger.LogError( + "Payment gateway failed for magic charge UserId {UserId}: {ErrorMessage}", + user.Id, + paymentResult.ErrorMessage + ); + + throw new Exception($"خطا در ارتباط با درگاه پرداخت: {paymentResult.ErrorMessage}"); + } + + // 6. ثبت PaymentTransaction + var paymentTx = new PaymentTransaction + { + GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal", + MerchantId = _configuration["ZarinPal:MerchantId"] ?? "", + Amount = request.Amount, + CallbackUrl = callbackUrl, + Description = $"شارژ کیف‌پول جادویی - کاربر {user.Id}", + Mobile = user.Mobile, + UserId = user.Id, + RequestStatusCode = 100, + RequestStatusMessage = "Success", + Authority = paymentResult.RefId, + PaymentStatus = false + }; + + _context.PaymentTransactions.Add(paymentTx); + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Magic wallet charge initiated. UserId: {UserId}, Amount: {Amount}, " + + "RemainingCap: {Remaining}, RefId: {RefId}, PaymentTxId: {PaymentTxId}", + user.Id, + request.Amount, + remainingDeposit - request.Amount, + paymentResult.RefId, + paymentTx.Id + ); + + return paymentResult; + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error in ChargeMagicWalletCommand for UserId: {UserId}", + request.UserId + ); + throw; + } + } +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandValidator.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandValidator.cs new file mode 100644 index 0000000..175f12f --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandValidator.cs @@ -0,0 +1,19 @@ +using FluentValidation; + +namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet; + +public class ChargeMagicWalletCommandValidator : AbstractValidator +{ + public ChargeMagicWalletCommandValidator() + { + RuleFor(x => x.UserId) + .GreaterThan(0) + .WithMessage("شناسه کاربر باید بزرگتر از صفر باشد"); + + RuleFor(x => x.Amount) + .GreaterThanOrEqualTo(100_000) + .WithMessage("حداقل مبلغ شارژ جادویی ۱۰,۰۰۰ تومان است") + .LessThanOrEqualTo(1_000_000_000) + .WithMessage("حداکثر مبلغ شارژ جادویی ۱۰۰,۰۰۰,۰۰۰ تومان است"); + } +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommand.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommand.cs new file mode 100644 index 0000000..e7b9f69 --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommand.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge; + +/// +/// تأیید شارژ کیف‌پول جادویی — بعد از بازگشت از درگاه پرداخت +/// +public class VerifyMagicWalletChargeCommand : IRequest +{ + /// + /// کد Authority از درگاه + /// + public string Authority { get; set; } = string.Empty; + + /// + /// وضعیت برگشتی از درگاه (OK / NOK) + /// + public string Status { get; set; } = string.Empty; +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommandHandler.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommandHandler.cs new file mode 100644 index 0000000..855c657 --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommandHandler.cs @@ -0,0 +1,209 @@ +using CMSMicroservice.Application.Common.Exceptions; +using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Common; +using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Entities.Payment; +using CMSMicroservice.Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge; + +public class VerifyMagicWalletChargeCommandHandler + : IRequestHandler +{ + private readonly IApplicationDbContext _context; + private readonly IPaymentGatewayService _paymentGateway; + private readonly ILogger _logger; + + public VerifyMagicWalletChargeCommandHandler( + IApplicationDbContext context, + IPaymentGatewayService paymentGateway, + ILogger logger) + { + _context = context; + _paymentGateway = paymentGateway; + _logger = logger; + } + + public async Task Handle( + VerifyMagicWalletChargeCommand request, + CancellationToken cancellationToken) + { + try + { + _logger.LogInformation( + "Verifying magic wallet charge. Authority: {Authority}, Status: {Status}", + request.Authority, + request.Status + ); + + // 1. پیدا کردن PaymentTransaction از Authority + var paymentTx = await _context.PaymentTransactions + .FirstOrDefaultAsync(pt => pt.Authority == request.Authority, cancellationToken); + + if (paymentTx == null) + { + _logger.LogError("PaymentTransaction not found for Authority: {Authority}", request.Authority); + throw new NotFoundException("تراکنش پرداخت یافت نشد"); + } + + if (paymentTx.PaymentStatus) + { + _logger.LogWarning("PaymentTransaction already verified: {Authority}", request.Authority); + return true; // قبلاً تأیید شده + } + + var userId = paymentTx.UserId + ?? throw new BadRequestException("شناسه کاربر در تراکنش پرداخت یافت نشد"); + var depositAmount = paymentTx.Amount; // مبلغ واقعی واریزی (ریال) + + // 2. بررسی وضعیت برگشتی از درگاه + if (!string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogWarning( + "Magic charge cancelled by user. UserId: {UserId}, Authority: {Authority}", + userId, request.Authority + ); + + paymentTx.PaymentStatus = false; + paymentTx.VerificationStatusMessage = "پرداخت توسط کاربر لغو شد"; + await _context.SaveChangesAsync(cancellationToken); + + throw new BadRequestException("پرداخت توسط کاربر لغو شد"); + } + + // 3. Verify با درگاه (زرین‌پال نیاز به مبلغ دارد) + var amountInToman = depositAmount / 10m; + var verifyResult = await _paymentGateway.VerifyPaymentAsync( + request.Authority, + request.Status, + amountInToman, + cancellationToken + ); + + // آپدیت PaymentTransaction + paymentTx.PaymentStatus = verifyResult.IsSuccess; + paymentTx.VerificationStatusCode = verifyResult.VerificationCode; + paymentTx.VerificationStatusMessage = verifyResult.Message; + paymentTx.CardPan = verifyResult.CardPan; + paymentTx.CardHash = verifyResult.CardHash; + paymentTx.RefId = verifyResult.TrackingCode; + + if (!verifyResult.IsSuccess) + { + _logger.LogWarning( + "Magic wallet charge verification failed for UserId {UserId}: {Message}", + userId, + verifyResult.Message + ); + + await _context.SaveChangesAsync(cancellationToken); + throw new BadRequestException($"تراکنش ناموفق: {verifyResult.Message}"); + } + + // 4. بررسی کیف‌پول و حالت جادویی + var wallet = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == userId, cancellationToken); + + if (wallet == null) + { + _logger.LogError("Wallet not found for UserId: {UserId}", userId); + throw new NotFoundException("کیف پول کاربر یافت نشد"); + } + + if (wallet.WalletMode != WalletMode.Magic) + { + _logger.LogError( + "Wallet is not in Magic mode during verify. UserId: {UserId}, Mode: {Mode}", + userId, wallet.WalletMode + ); + throw new BadRequestException("کیف‌پول در حالت جادویی نیست"); + } + + // 5. محاسبه اعتبار ×2.5 + var creditAmount = (long)(depositAmount * SystemConstants.MagicWalletMultiplier); // مبلغ × 2.5 + var bonusAmount = creditAmount - depositAmount; // بونوس = مبلغ × 1.5 + + // 6. ثبت تراکنش واریز واقعی (MagicWalletDeposit) + var depositTransaction = new Transaction + { + Amount = depositAmount, + Description = $"شارژ کیف‌پول جادویی - واریز واقعی - کاربر {userId}", + PaymentStatus = PaymentStatus.Success, + PaymentDate = DateTime.UtcNow, + RefId = verifyResult.RefId, + Type = TransactionType.MagicWalletDeposit + }; + + _context.Transactions.Add(depositTransaction); + await _context.SaveChangesAsync(cancellationToken); + + // 7. ثبت تراکنش بونوس (MagicWalletBonus) + var bonusTransaction = new Transaction + { + Amount = bonusAmount, + Description = $"شارژ کیف‌پول جادویی - بونوس ×{SystemConstants.MagicWalletMultiplier - 1} - کاربر {userId}", + PaymentStatus = PaymentStatus.Success, + PaymentDate = DateTime.UtcNow, + RefId = $"MAGIC_BONUS_{depositTransaction.Id}", + Type = TransactionType.MagicWalletBonus + }; + + _context.Transactions.Add(bonusTransaction); + await _context.SaveChangesAsync(cancellationToken); + + // 8. شارژ Balance و آپدیت شمارنده‌ها + wallet.Balance += creditAmount; + wallet.MagicTotalDeposited += depositAmount; + wallet.MagicTotalCredited += creditAmount; + + // 9. ثبت WalletChangeLog (اجباری) + var walletLog = new UserWalletChangeLog + { + WalletId = wallet.Id, + CurrentBalance = wallet.Balance, + ChangeValue = creditAmount, + CurrentNetworkBalance = wallet.NetworkBalance, + ChangeNerworkValue = 0, + CurrentDiscountBalance = wallet.DiscountBalance, + ChangeDiscountValue = 0, + IsIncrease = true, + RefrenceId = depositTransaction.Id + }; + + _context.UserWalletChangeLogs.Add(walletLog); + + // 10. لینک PaymentTransaction به Transaction داخلی + paymentTx.TransactionId = depositTransaction.Id; + + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Magic wallet charged successfully. UserId: {UserId}, " + + "Deposit: {Deposit}, Credit: {Credit} (×{Multiplier}), Bonus: {Bonus}, " + + "TotalDeposited: {TotalDeposited}/{MaxDeposit}, NewBalance: {NewBalance}", + userId, + depositAmount, + creditAmount, + SystemConstants.MagicWalletMultiplier, + bonusAmount, + wallet.MagicTotalDeposited, + SystemConstants.MagicWalletMaxDeposit, + wallet.Balance + ); + + return true; + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error in VerifyMagicWalletChargeCommand. Authority: {Authority}", + request.Authority + ); + throw; + } + } +} diff --git a/src/CMSMicroservice.Domain/Common/SystemConstants.cs b/src/CMSMicroservice.Domain/Common/SystemConstants.cs index 34c35b1..e0b4b3d 100644 --- a/src/CMSMicroservice.Domain/Common/SystemConstants.cs +++ b/src/CMSMicroservice.Domain/Common/SystemConstants.cs @@ -92,6 +92,25 @@ public static class SystemConstants #endregion + #region Magic Wallet Settings + + /// + /// ضریب شارژ کیف‌پول جادویی — واریز × 2.5 = اعتبار + /// + public const decimal MagicWalletMultiplier = 2.5m; + + /// + /// سقف واریز در هر دور جادویی (ریال) — 100M تومان + /// + public const long MagicWalletMaxDeposit = 1_000_000_000; + + /// + /// سقف اعتبار در هر دور جادویی (ریال) — 250M تومان + /// + public const long MagicWalletMaxCredit = 2_500_000_000; + + #endregion + #region Shop Settings /// @@ -134,6 +153,11 @@ public static class SystemConstants ["System.MaintenanceMode"] = SystemMaintenanceMode, ["System.EnableAuditLog"] = SystemEnableAuditLog, + // Magic Wallet + ["MagicWallet.Multiplier"] = MagicWalletMultiplier, + ["MagicWallet.MaxDeposit"] = MagicWalletMaxDeposit, + ["MagicWallet.MaxCredit"] = MagicWalletMaxCredit, + // Shop ["Shop.VAT"] = ShopVAT, ["Shop.VATEnabled"] = ShopVATEnabled @@ -166,6 +190,11 @@ public static class SystemConstants ("System.MaintenanceMode", SystemMaintenanceMode, "حالت تعمیر و نگهداری سیستم"), ("System.EnableAuditLog", SystemEnableAuditLog, "فعال‌سازی لاگ تغییرات"), + // Magic Wallet + ("MagicWallet.Multiplier", MagicWalletMultiplier, "ضریب شارژ کیف‌پول جادویی (×2.5)"), + ("MagicWallet.MaxDeposit", MagicWalletMaxDeposit, "سقف واریز هر دور جادویی (ریال)"), + ("MagicWallet.MaxCredit", MagicWalletMaxCredit, "سقف اعتبار هر دور جادویی (ریال)"), + // Shop ("Shop.VAT", ShopVAT, "مالیات بر ارزش افزوده"), ("Shop.VATEnabled", ShopVATEnabled, "مالیات فعال است؟") diff --git a/src/CMSMicroservice.Domain/Entities/Club/ClubMembership.cs b/src/CMSMicroservice.Domain/Entities/Club/ClubMembership.cs index 66af6b7..089f6bf 100644 --- a/src/CMSMicroservice.Domain/Entities/Club/ClubMembership.cs +++ b/src/CMSMicroservice.Domain/Entities/Club/ClubMembership.cs @@ -55,4 +55,9 @@ public class ClubMembership : BaseAuditableEntity /// ClubMembershipHistory Collection Navigation Reference /// public virtual ICollection? ClubMembershipHistories { get; set; } + + /// + /// دوره‌های خرید پکیج — هر خرید پکیج یک Cycle جدید + /// + public virtual ICollection? Cycles { get; set; } } diff --git a/src/CMSMicroservice.Domain/Entities/Club/ClubMembershipCycle.cs b/src/CMSMicroservice.Domain/Entities/Club/ClubMembershipCycle.cs new file mode 100644 index 0000000..070f713 --- /dev/null +++ b/src/CMSMicroservice.Domain/Entities/Club/ClubMembershipCycle.cs @@ -0,0 +1,64 @@ +namespace CMSMicroservice.Domain.Entities.Club; + +/// +/// دوره خرید پکیج — هر بار خرید پکیج ۵۶M یک Cycle جدید ایجاد می‌شود. +/// برای حل مشکل overwrite شدن ClubMembership.ActivatedAt در محاسبه کمیسیون. +/// +public class ClubMembershipCycle : BaseAuditableEntity +{ + /// + /// شناسه کاربر + /// + public long UserId { get; set; } + + /// + /// User Navigation Property + /// + public virtual User User { get; set; } + + /// + /// شناسه عضویت باشگاه + /// + public long ClubMembershipId { get; set; } + + /// + /// ClubMembership Navigation Property + /// + public virtual ClubMembership ClubMembership { get; set; } + + /// + /// شماره دور (1, 2, 3, ...) + /// + public int CycleNumber { get; set; } + + /// + /// تاریخ خرید پکیج — این فیلد برای محاسبه کمیسیون هفتگی استفاده می‌شود + /// (به‌جای ClubMembership.ActivatedAt که overwrite می‌شد) + /// + public DateTime PackagePurchasedAt { get; set; } + + /// + /// تاریخ شروع حالت جادویی (وقتی Balance=0 شد) + /// + public DateTime? MagicStartedAt { get; set; } + + /// + /// تاریخ پایان حالت جادویی + /// + public DateTime? MagicCompletedAt { get; set; } + + /// + /// نحوه خرید پکیج در این دور + /// + public PackagePurchaseMethod PurchaseMethod { get; set; } + + /// + /// مبلغ پکیج (ریال) + /// + public long PackageAmount { get; set; } + + /// + /// آیا این دور فعلی است؟ فقط یک رکورد true می‌باشد + /// + public bool IsCurrentCycle { get; set; } +} diff --git a/src/CMSMicroservice.Domain/Entities/UserWallet.cs b/src/CMSMicroservice.Domain/Entities/UserWallet.cs index a24d42e..a3c1e27 100644 --- a/src/CMSMicroservice.Domain/Entities/UserWallet.cs +++ b/src/CMSMicroservice.Domain/Entities/UserWallet.cs @@ -18,7 +18,36 @@ public class UserWallet : BaseAuditableEntity /// موجودی تخفیف - فقط برای خرید از فروشگاه باشگاه مشتریان /// public long DiscountBalance { get; set; } - + + #region Magic Wallet + + /// + /// حالت کیف‌پول — Normal: کمیسیون فعال | Magic: شارژ ×2.5 بدون کمیسیون + /// + public WalletMode WalletMode { get; set; } = WalletMode.Normal; + + /// + /// مجموع واریزی واقعی در دور جادویی فعلی (ریال) — سقف: MagicWalletMaxDeposit + /// + public long MagicTotalDeposited { get; set; } + + /// + /// مجموع اعتبار داده‌شده در دور جادویی فعلی (ریال) — واریز × 2.5 + /// + public long MagicTotalCredited { get; set; } + + /// + /// زمان شروع دور جادویی فعلی + /// + public DateTime? MagicActivatedAt { get; set; } + + /// + /// زمان پایان دور جادویی فعلی + /// + public DateTime? MagicCompletedAt { get; set; } + + #endregion + //UserWalletChangeLog Collection Navigation Reference public virtual ICollection UserWalletChangeLogs { get; set; } } diff --git a/src/CMSMicroservice.Domain/Enums/TransactionType.cs b/src/CMSMicroservice.Domain/Enums/TransactionType.cs index 6846125..f93f9b6 100644 --- a/src/CMSMicroservice.Domain/Enums/TransactionType.cs +++ b/src/CMSMicroservice.Domain/Enums/TransactionType.cs @@ -26,4 +26,14 @@ public enum TransactionType /// خرید از فروشگاه تخفیف /// DiscountShopPurchase = 13, + + /// + /// واریز از درگاه به کیف‌پول جادویی (مبلغ واقعی) + /// + MagicWalletDeposit = 14, + + /// + /// بونوس داخلی کیف‌پول جادویی (مبلغ × 1.5) + /// + MagicWalletBonus = 15, } diff --git a/src/CMSMicroservice.Domain/Enums/WalletMode.cs b/src/CMSMicroservice.Domain/Enums/WalletMode.cs new file mode 100644 index 0000000..dcc99c0 --- /dev/null +++ b/src/CMSMicroservice.Domain/Enums/WalletMode.cs @@ -0,0 +1,13 @@ +namespace CMSMicroservice.Domain.Enums; + +/// +/// حالت کیف‌پول — Normal: کمیسیون فعال | Magic: شارژ ×2.5 بدون کمیسیون +/// +public enum WalletMode +{ + /// حالت عادی — کمیسیون و پورسانت فعال + Normal = 0, + + /// حالت جادویی — شارژ ×2.5، بدون کمیسیون + Magic = 1 +} diff --git a/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs b/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs index bf9b897..d772ffa 100644 --- a/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs @@ -109,6 +109,7 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext public DbSet ClubFeatures => Set(); public DbSet UserClubFeatures => Set(); public DbSet ClubMembershipHistories => Set(); + public DbSet ClubMembershipCycles => Set(); // Network public DbSet NetworkWeeklyBalances => Set(); diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Configurations/ClubMembershipCycleConfiguration.cs b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/ClubMembershipCycleConfiguration.cs new file mode 100644 index 0000000..19fb522 --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/ClubMembershipCycleConfiguration.cs @@ -0,0 +1,45 @@ +using CMSMicroservice.Domain.Entities.Club; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CMSMicroservice.Infrastructure.Persistence.Configurations; + +public class ClubMembershipCycleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasQueryFilter(p => !p.IsDeleted); + builder.Ignore(entity => entity.DomainEvents); + builder.HasKey(entity => entity.Id); + builder.Property(entity => entity.Id).UseIdentityColumn(); + + builder.Property(entity => entity.UserId).IsRequired(); + builder.Property(entity => entity.ClubMembershipId).IsRequired(); + builder.Property(entity => entity.CycleNumber).IsRequired(); + builder.Property(entity => entity.PackagePurchasedAt).IsRequired(); + builder.Property(entity => entity.MagicStartedAt).IsRequired(false); + builder.Property(entity => entity.MagicCompletedAt).IsRequired(false); + builder.Property(entity => entity.PurchaseMethod).IsRequired(); + builder.Property(entity => entity.PackageAmount).IsRequired(); + builder.Property(entity => entity.IsCurrentCycle) + .IsRequired() + .HasDefaultValue(false); + + // Relationships + builder.HasOne(entity => entity.User) + .WithMany() + .HasForeignKey(entity => entity.UserId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(entity => entity.ClubMembership) + .WithMany(cm => cm.Cycles) + .HasForeignKey(entity => entity.ClubMembershipId) + .OnDelete(DeleteBehavior.Restrict); + + // Indexes + builder.HasIndex(e => new { e.UserId, e.IsCurrentCycle }) + .HasDatabaseName("IX_ClubMembershipCycle_UserId_IsCurrentCycle"); + builder.HasIndex(e => e.PackagePurchasedAt) + .HasDatabaseName("IX_ClubMembershipCycle_PackagePurchasedAt"); + } +} diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserWalletConfiguration.cs b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserWalletConfiguration.cs index a39b681..6997722 100644 --- a/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserWalletConfiguration.cs +++ b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserWalletConfiguration.cs @@ -1,4 +1,5 @@ using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CMSMicroservice.Infrastructure.Persistence.Configurations; @@ -20,5 +21,18 @@ public class UserWalletConfiguration : IEntityTypeConfiguration builder.Property(entity => entity.NetworkBalance).IsRequired(true); builder.Property(entity => entity.DiscountBalance).IsRequired(true); + // Magic Wallet + builder.Property(entity => entity.WalletMode) + .IsRequired() + .HasDefaultValue(WalletMode.Normal); + builder.Property(entity => entity.MagicTotalDeposited) + .IsRequired() + .HasDefaultValue(0L); + builder.Property(entity => entity.MagicTotalCredited) + .IsRequired() + .HasDefaultValue(0L); + builder.Property(entity => entity.MagicActivatedAt).IsRequired(false); + builder.Property(entity => entity.MagicCompletedAt).IsRequired(false); + } } diff --git a/src/CMSMicroservice.Infrastructure/Services/Commission/OrmCommissionCalculationStrategy.cs b/src/CMSMicroservice.Infrastructure/Services/Commission/OrmCommissionCalculationStrategy.cs index ea6f36b..5db7dff 100644 --- a/src/CMSMicroservice.Infrastructure/Services/Commission/OrmCommissionCalculationStrategy.cs +++ b/src/CMSMicroservice.Infrastructure/Services/Commission/OrmCommissionCalculationStrategy.cs @@ -56,8 +56,14 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy } // دریافت همه کاربرانی که عضو فعال باشگاه هستند + // ⚠️ Magic Wallet: کاربرهایی که در حالت جادویی هستند از کمیسیون خارج میشن + var magicModeUserIds = await _context.UserWallets + .Where(w => w.WalletMode == WalletMode.Magic) + .Select(w => w.UserId) + .ToHashSetAsync(cancellationToken); + var activeClubMemberUserIds = await _context.ClubMemberships - .Where(c => c.IsActive) + .Where(c => c.IsActive && !magicModeUserIds.Contains(c.UserId)) .Select(c => c.UserId) .ToHashSetAsync(cancellationToken); @@ -364,10 +370,11 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy } var count = 0; - var membership = await _context.ClubMemberships - .FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsActive, cancellationToken); + // ⚠️ از ClubMembershipCycle.PackagePurchasedAt استفاده میکنیم (نه ActivatedAt که overwrite میشد) + var currentCycle = await _context.ClubMembershipCycles + .FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsCurrentCycle, cancellationToken); - if (membership?.ActivatedAt >= startDate && membership?.ActivatedAt <= endDate) + if (currentCycle?.PackagePurchasedAt >= startDate && currentCycle?.PackagePurchasedAt <= endDate) { count = 1; } diff --git a/src/CMSMicroservice.Protobuf/Protos/userwallet.proto b/src/CMSMicroservice.Protobuf/Protos/userwallet.proto index 76c31f4..0d79f56 100644 --- a/src/CMSMicroservice.Protobuf/Protos/userwallet.proto +++ b/src/CMSMicroservice.Protobuf/Protos/userwallet.proto @@ -74,6 +74,20 @@ service UserWalletContract get: "/Customer/GetWithdrawalSettings" }; }; + + // ============= Magic Wallet Methods ============= + + rpc InitiateMagicCharge(InitiateMagicChargeRequest) returns (InitiateMagicChargeResponse){ + option (google.api.http) = { + post: "/Customer/InitiateMagicCharge" + body: "*" + }; + }; + rpc GetMagicWalletStatus(google.protobuf.Empty) returns (GetMagicWalletStatusResponse){ + option (google.api.http) = { + get: "/Customer/GetMagicWalletStatus" + }; + }; } message CreateNewUserWalletRequest { @@ -140,6 +154,7 @@ message GetCustomerWalletResponse int64 balance = 1; int64 network_balance = 2; int64 discount_balance = 3; + int32 wallet_mode = 4; // 0=Normal, 1=Magic } message GetCustomerWalletChangeLogRequest @@ -200,4 +215,29 @@ message CustomerWithdrawalModel message GetCustomerWithdrawalSettingsResponse { int64 min_withdrawal_amount = 1; +} + +// ============= Magic Wallet Messages ============= + +message InitiateMagicChargeRequest +{ + int64 amount = 1; // مبلغ واریزی واقعی (ریال) +} + +message InitiateMagicChargeResponse +{ + bool is_success = 1; + string gateway_url = 2; + string error_message = 3; +} + +message GetMagicWalletStatusResponse +{ + int32 wallet_mode = 1; // 0=Normal, 1=Magic + int64 magic_total_deposited = 2; + int64 magic_total_credited = 3; + int64 magic_max_deposit = 4; + int64 magic_remaining_deposit = 5; + int64 balance = 6; + google.protobuf.Timestamp magic_activated_at = 7; } \ No newline at end of file diff --git a/src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs b/src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs index 2a63b17..c9525ed 100644 --- a/src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs +++ b/src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs @@ -1,5 +1,6 @@ using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment; +using CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -143,4 +144,46 @@ public class PaymentCallbackController : ControllerBase $"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=error"); } } + + /// + /// Callback برای شارژ کیف‌پول جادویی — زرین‌پال بعد از پرداخت کاربر را اینجا برمی‌گرداند + /// + [HttpGet("/api/wallet/verify-magic-charge")] + public async Task MagicChargeCallback( + [FromQuery(Name = "Authority")] string? authority, + [FromQuery(Name = "Status")] string? status, + CancellationToken cancellationToken) + { + var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268"; + + _logger.LogInformation( + "Magic charge callback received: Authority={Authority}, Status={Status}", + authority, status); + + try + { + if (string.IsNullOrEmpty(authority)) + { + _logger.LogError("Magic charge callback: Authority is missing"); + return Redirect($"{frontOfficeBaseUrl}/magic-wallet?payment=error&reason=no-authority"); + } + + var result = await _sender.Send(new VerifyMagicWalletChargeCommand + { + Authority = authority, + Status = status ?? "NOK" + }, cancellationToken); + + _logger.LogInformation( + "Magic charge completed successfully. Authority={Authority}", + authority); + + return Redirect($"{frontOfficeBaseUrl}/magic-wallet?payment=success"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Magic charge callback error. Authority={Authority}", authority); + return Redirect($"{frontOfficeBaseUrl}/magic-wallet?payment=failed"); + } + } } diff --git a/src/CMSMicroservice.WebApi/Services/UserOrderService.cs b/src/CMSMicroservice.WebApi/Services/UserOrderService.cs index aacf89c..0dfd15d 100644 --- a/src/CMSMicroservice.WebApi/Services/UserOrderService.cs +++ b/src/CMSMicroservice.WebApi/Services/UserOrderService.cs @@ -6,7 +6,9 @@ using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrderHistory; using CMSMicroservice.Application.OrderManagementCQ.Commands.UpdateOrderStatus; using CMSMicroservice.Application.OrderManagementCQ.Commands.CancelOrderByAdmin; using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Common; using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Entities.Club; using CMSMicroservice.Domain.Entities.Order; using CMSMicroservice.Domain.Enums; using AppModels = CMSMicroservice.Application.Common.Models; @@ -272,7 +274,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase } // Calculate amounts - const decimal vatRate = 0.09m; + const decimal vatRate = 0.10m; long baseAmount = cartItems.Sum(c => c.Product.Price * c.Count); long vatAmount = (long)(baseAmount * vatRate); long totalAmount = baseAmount + vatAmount; @@ -334,7 +336,54 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase }; _context.UserWalletChangeLogs.Add(walletLog); - + + // ═══ Magic Wallet: Entry / Exit Trigger ═══ + if (wallet.Balance == 0) + { + var user = await _context.Users + .Include(u => u.ClubMembership) + .FirstOrDefaultAsync(u => u.Id == userId, context.CancellationToken); + + if (wallet.WalletMode == WalletMode.Magic + && wallet.MagicTotalDeposited >= SystemConstants.MagicWalletMaxDeposit) + { + // ── EXIT Magic Mode ── + // هر دو شرط: Balance=0 و سقف 100M پر شده + wallet.WalletMode = WalletMode.Normal; + wallet.MagicCompletedAt = DateTime.UtcNow; + + var currentCycle = await _context.ClubMembershipCycles + .FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, + context.CancellationToken); + if (currentCycle != null) + { + currentCycle.MagicCompletedAt = DateTime.UtcNow; + } + } + else if (wallet.WalletMode == WalletMode.Normal + && user != null + && user.PackagePurchaseMethod != PackagePurchaseMethod.None + && user.ClubMembership?.IsActive == true) + { + // ── ENTER Magic Mode ── + wallet.WalletMode = WalletMode.Magic; + wallet.MagicActivatedAt = DateTime.UtcNow; + wallet.MagicCompletedAt = null; + wallet.MagicTotalDeposited = 0; + wallet.MagicTotalCredited = 0; + + var currentCycle = await _context.ClubMembershipCycles + .FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, + context.CancellationToken); + if (currentCycle != null) + { + currentCycle.MagicStartedAt = DateTime.UtcNow; + } + } + // ⚠️ اگه Magic باشه و Balance=0 ولی سقف پر نشده → هنوز Magic! + // کاربر میتونه دوباره شارژ کنه + } + // Create order var order = new UserOrder { @@ -935,11 +984,11 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase public override Task GetVATRate(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context) { - // VAT Rate for Iran: 9% (نرخ مالیات بر ارزش افزوده ایران) + // VAT Rate for Iran: 10% (نرخ مالیات بر ارزش افزوده ایران) return Task.FromResult(new GetVATRateResponse { - VatRate = 0.09, - VatPercentage = 9, + VatRate = 0.10, + VatPercentage = 10, IsEnabled = true }); } diff --git a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs index 1f51dae..b48d611 100644 --- a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs +++ b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs @@ -3,13 +3,17 @@ using CMSMicroservice.WebApi.Common.Services; using CMSMicroservice.Application.UserWalletCQ.Commands.CreateNewUserWallet; using CMSMicroservice.Application.UserWalletCQ.Commands.UpdateUserWallet; using CMSMicroservice.Application.UserWalletCQ.Commands.DeleteUserWallet; +using CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet; using CMSMicroservice.Application.UserWalletCQ.Queries.GetUserWallet; using CMSMicroservice.Application.UserWalletCQ.Queries.GetAllUserWalletByFilter; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletChangeLog; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawals; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawalSettings; using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Common; +using CMSMicroservice.Domain.Enums; using Grpc.Core; +using Google.Protobuf.WellKnownTypes; using Microsoft.EntityFrameworkCore; using System.Linq; @@ -64,11 +68,15 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase var walletQuery = new GetUserWalletQuery { Id = userId }; var wallet = await _sender.Send(walletQuery, context.CancellationToken); + var walletEntity = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == userId, context.CancellationToken); + return new GetCustomerWalletResponse { Balance = wallet.Balance, NetworkBalance = wallet.NetworkBalance, - DiscountBalance = wallet.DiscountBalance + DiscountBalance = wallet.DiscountBalance, + WalletMode = (int)(walletEntity?.WalletMode ?? WalletMode.Normal) }; } @@ -141,6 +149,57 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase return new Google.Protobuf.WellKnownTypes.Empty(); } + // ============= Magic Wallet Methods ============= + + public override async Task InitiateMagicCharge( + InitiateMagicChargeRequest request, ServerCallContext context) + { + var userId = GetCurrentUserId(); + + var result = await _sender.Send(new ChargeMagicWalletCommand + { + UserId = userId, + Amount = request.Amount + }, context.CancellationToken); + + return new InitiateMagicChargeResponse + { + IsSuccess = result.IsSuccess, + GatewayUrl = result.GatewayUrl ?? "", + ErrorMessage = result.ErrorMessage ?? "" + }; + } + + public override async Task GetMagicWalletStatus( + Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context) + { + var userId = GetCurrentUserId(); + + var wallet = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == userId, context.CancellationToken); + + if (wallet == null) + throw new RpcException(new Status(StatusCode.NotFound, "کیف پول یافت نشد")); + + var response = new GetMagicWalletStatusResponse + { + WalletMode = (int)wallet.WalletMode, + MagicTotalDeposited = wallet.MagicTotalDeposited, + MagicTotalCredited = wallet.MagicTotalCredited, + MagicMaxDeposit = SystemConstants.MagicWalletMaxDeposit, + MagicRemainingDeposit = Math.Max(0, SystemConstants.MagicWalletMaxDeposit - wallet.MagicTotalDeposited), + Balance = wallet.Balance + }; + + if (wallet.MagicActivatedAt.HasValue) + { + response.MagicActivatedAt = Timestamp.FromDateTime( + DateTime.SpecifyKind(wallet.MagicActivatedAt.Value, DateTimeKind.Utc)); + } + + return response; + } + private long GetCurrentUserId() { if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0) From bdd2c517266a829644fe7f4dd8de555cce23d999 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sat, 21 Feb 2026 20:37:14 +0330 Subject: [PATCH 4/4] fix: Update project version to 0.0.181 and adjust NuGet package path --- src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj index 4d9ddd7..338a7dd 100644 --- a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj +++ b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj @@ -3,7 +3,7 @@ net9.0 enable enable - 0.0.180 + 0.0.181 None False False @@ -78,7 +78,7 @@ - $(PackageOutputPath)/$(PackageId).$(Version).nupkg + /home/masoud/Apps/project/FourSat/CMS/src/CMSMicroservice.Protobuf/bin/Debug/$(PackageId).$(Version).nupkg dotnet nuget push "$(NugetPackagePath)" --source foursat-hosted --api-key admin:87zH26nbqT --skip-duplicate --configfile "$(MSBuildThisFileDirectory)../NuGet.config"