Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9050c00297 | |||
| bdd2c51726 | |||
| 2a569a024f | |||
| 04e8c49fa7 | |||
| f968a6c005 | |||
| 6048824b33 |
@@ -1,445 +1,3 @@
|
|||||||
# CMS Microservice - Network & Club Commission + Inventory Management System
|
# CMS Microservice
|
||||||
|
|
||||||
[]()
|
Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
|
||||||
[]()
|
|
||||||
[]()
|
|
||||||
|
|
||||||
## 📊 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<WeeklyCommissionJob>(
|
|
||||||
"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
|
|
||||||
|
|||||||
+36
-2
@@ -183,10 +183,9 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// فعالسازی مجدد
|
// فعالسازی مجدد — ActivatedAt حفظ میشه (overwrite نمیشه)
|
||||||
entity = existingMembership;
|
entity = existingMembership;
|
||||||
entity.IsActive = true;
|
entity.IsActive = true;
|
||||||
entity.ActivatedAt = activationDate;
|
|
||||||
entity.PurchaseMethod = user.PackagePurchaseMethod;
|
entity.PurchaseMethod = user.PackagePurchaseMethod;
|
||||||
|
|
||||||
_context.ClubMemberships.Update(entity);
|
_context.ClubMemberships.Update(entity);
|
||||||
@@ -199,6 +198,41 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 6.5. ایجاد ClubMembershipCycle — هر خرید پکیج یک دور جدید
|
||||||
|
var previousCycles = await _context.ClubMembershipCycles
|
||||||
|
.Where(c => 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. ثبت تاریخچه
|
// 7. ثبت تاریخچه
|
||||||
var history = new ClubMembershipHistory
|
var history = new ClubMembershipHistory
|
||||||
{
|
{
|
||||||
|
|||||||
+12
-5
@@ -43,8 +43,14 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
|||||||
|
|
||||||
// ⭐ دریافت همه کاربرانی که عضو فعال باشگاه هستند
|
// ⭐ دریافت همه کاربرانی که عضو فعال باشگاه هستند
|
||||||
// بدون محدودیت زمانی - همه اعضای فعال کلاب باید کمیسیون بگیرند
|
// بدون محدودیت زمانی - همه اعضای فعال کلاب باید کمیسیون بگیرند
|
||||||
|
// ⚠️ Magic Wallet: کاربرهایی که در حالت جادویی هستند از کمیسیون خارج میشن
|
||||||
|
var magicModeUserIds = await _context.UserWallets
|
||||||
|
.Where(w => w.WalletMode == WalletMode.Magic)
|
||||||
|
.Select(w => w.UserId)
|
||||||
|
.ToHashSetAsync(cancellationToken);
|
||||||
|
|
||||||
var activeClubMemberUserIds = await _context.ClubMemberships
|
var activeClubMemberUserIds = await _context.ClubMemberships
|
||||||
.Where(c => c.IsActive)
|
.Where(c => c.IsActive && !magicModeUserIds.Contains(c.UserId))
|
||||||
.Select(c => c.UserId)
|
.Select(c => c.UserId)
|
||||||
.ToHashSetAsync(cancellationToken);
|
.ToHashSetAsync(cancellationToken);
|
||||||
|
|
||||||
@@ -256,11 +262,12 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
|||||||
|
|
||||||
var count = 0;
|
var count = 0;
|
||||||
|
|
||||||
// اگر فرزند در این هفته فعال شده، 1 امتیاز
|
// اگر فرزند در این هفته پکیج خریده (Cycle جدید ساخته شده)، 1 امتیاز
|
||||||
var membership = await _context.ClubMemberships
|
// ⚠️ از ClubMembershipCycle.PackagePurchasedAt استفاده میکنیم (نه ActivatedAt که overwrite میشد)
|
||||||
.FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsActive, cancellationToken);
|
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;
|
count = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ public interface IApplicationDbContext
|
|||||||
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<ClubFeature> ClubFeatures { get; }
|
DbSet<ClubFeature> ClubFeatures { get; }
|
||||||
DbSet<UserClubFeature> UserClubFeatures { get; }
|
DbSet<UserClubFeature> UserClubFeatures { get; }
|
||||||
DbSet<NetworkWeeklyBalance> NetworkWeeklyBalances { get; }
|
DbSet<NetworkWeeklyBalance> NetworkWeeklyBalances { get; }
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ namespace CMSMicroservice.Application.Common.Services;
|
|||||||
public static class VatCalculator
|
public static class VatCalculator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// نرخ VAT ایران - 9 درصد
|
/// نرخ VAT ایران - 10 درصد
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const decimal VAT_RATE = 0.09m;
|
public const decimal VAT_RATE = 0.10m;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// نرخ VAT به صورت درصد (9)
|
/// نرخ VAT به صورت درصد (10)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const int VAT_PERCENT = 9;
|
public const int VAT_PERCENT = 10;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// محاسبه VAT از مبلغ خالص
|
/// محاسبه VAT از مبلغ خالص
|
||||||
|
|||||||
+2
-2
@@ -133,7 +133,7 @@ public class VerifyBasePackagePaymentCommandHandler : IRequestHandler<VerifyBase
|
|||||||
var oldDiscountBalance = userWallet.DiscountBalance;
|
var oldDiscountBalance = userWallet.DiscountBalance;
|
||||||
|
|
||||||
userWallet.Balance += SystemConstants.BasePackageAmount;
|
userWallet.Balance += SystemConstants.BasePackageAmount;
|
||||||
userWallet.DiscountBalance += SystemConstants.BasePackageAmount;
|
userWallet.DiscountBalance += SystemConstants.BasePackageAmount * 2;
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Charging wallet for user {UserId}. Balance: {OldBalance} -> {NewBalance}, DiscountBalance: {OldDiscount} -> {NewDiscount}",
|
"Charging wallet for user {UserId}. Balance: {OldBalance} -> {NewBalance}, DiscountBalance: {OldDiscount} -> {NewDiscount}",
|
||||||
@@ -157,7 +157,7 @@ public class VerifyBasePackagePaymentCommandHandler : IRequestHandler<VerifyBase
|
|||||||
CurrentNetworkBalance = userWallet.NetworkBalance,
|
CurrentNetworkBalance = userWallet.NetworkBalance,
|
||||||
ChangeNerworkValue = 0,
|
ChangeNerworkValue = 0,
|
||||||
CurrentDiscountBalance = userWallet.DiscountBalance,
|
CurrentDiscountBalance = userWallet.DiscountBalance,
|
||||||
ChangeDiscountValue = SystemConstants.BasePackageAmount,
|
ChangeDiscountValue = SystemConstants.BasePackageAmount * 2,
|
||||||
IsIncrease = true,
|
IsIncrease = true,
|
||||||
RefrenceId = transaction.Id
|
RefrenceId = transaction.Id
|
||||||
};
|
};
|
||||||
|
|||||||
+4
-4
@@ -99,9 +99,9 @@ public class VerifyPackagePurchaseCommandHandler
|
|||||||
wallet.Balance
|
wallet.Balance
|
||||||
);
|
);
|
||||||
|
|
||||||
// شارژ DiscountBalance (موجودی تخفیف)
|
// شارژ DiscountBalance (موجودی تخفیف) — دو برابر مبلغ سفارش
|
||||||
var oldDiscountBalance = wallet.DiscountBalance;
|
var oldDiscountBalance = wallet.DiscountBalance;
|
||||||
wallet.DiscountBalance += order.Amount;
|
wallet.DiscountBalance += order.Amount * 2;
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Charging DiscountBalance for UserId {UserId}: {OldBalance} -> {NewBalance}",
|
"Charging DiscountBalance for UserId {UserId}: {OldBalance} -> {NewBalance}",
|
||||||
@@ -132,7 +132,7 @@ public class VerifyPackagePurchaseCommandHandler
|
|||||||
ChangeValue = order.Amount,
|
ChangeValue = order.Amount,
|
||||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||||
ChangeNerworkValue = 0,
|
ChangeNerworkValue = 0,
|
||||||
CurrentDiscountBalance = wallet.DiscountBalance - order.Amount, // قبل از شارژ DiscountBalance
|
CurrentDiscountBalance = wallet.DiscountBalance - (order.Amount * 2), // قبل از شارژ DiscountBalance
|
||||||
ChangeDiscountValue = 0,
|
ChangeDiscountValue = 0,
|
||||||
IsIncrease = true,
|
IsIncrease = true,
|
||||||
RefrenceId = transaction.Id
|
RefrenceId = transaction.Id
|
||||||
@@ -148,7 +148,7 @@ public class VerifyPackagePurchaseCommandHandler
|
|||||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||||
ChangeNerworkValue = 0,
|
ChangeNerworkValue = 0,
|
||||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||||
ChangeDiscountValue = order.Amount,
|
ChangeDiscountValue = order.Amount * 2,
|
||||||
IsIncrease = true,
|
IsIncrease = true,
|
||||||
RefrenceId = transaction.Id
|
RefrenceId = transaction.Id
|
||||||
};
|
};
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شروع شارژ کیفپول جادویی از طریق درگاه پرداخت
|
||||||
|
/// مبلغ واریزی × 2.5 به Balance اعتبار داده میشود
|
||||||
|
/// </summary>
|
||||||
|
public class ChargeMagicWalletCommand : IRequest<PaymentInitiateResult>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// مبلغ واریزی واقعی (ریال) — اعتبار = مبلغ × 2.5
|
||||||
|
/// </summary>
|
||||||
|
public long Amount { get; set; }
|
||||||
|
}
|
||||||
+174
@@ -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<ChargeMagicWalletCommand, PaymentInitiateResult>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IPaymentGatewayService _paymentGateway;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ILogger<ChargeMagicWalletCommandHandler> _logger;
|
||||||
|
|
||||||
|
public ChargeMagicWalletCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IPaymentGatewayService paymentGateway,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<ChargeMagicWalletCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_paymentGateway = paymentGateway;
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PaymentInitiateResult> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet;
|
||||||
|
|
||||||
|
public class ChargeMagicWalletCommandValidator : AbstractValidator<ChargeMagicWalletCommand>
|
||||||
|
{
|
||||||
|
public ChargeMagicWalletCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.UserId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
|
||||||
|
|
||||||
|
RuleFor(x => x.Amount)
|
||||||
|
.GreaterThanOrEqualTo(100_000)
|
||||||
|
.WithMessage("حداقل مبلغ شارژ جادویی ۱۰,۰۰۰ تومان است")
|
||||||
|
.LessThanOrEqualTo(1_000_000_000)
|
||||||
|
.WithMessage("حداکثر مبلغ شارژ جادویی ۱۰۰,۰۰۰,۰۰۰ تومان است");
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تأیید شارژ کیفپول جادویی — بعد از بازگشت از درگاه پرداخت
|
||||||
|
/// </summary>
|
||||||
|
public class VerifyMagicWalletChargeCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// کد Authority از درگاه
|
||||||
|
/// </summary>
|
||||||
|
public string Authority { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// وضعیت برگشتی از درگاه (OK / NOK)
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
+209
@@ -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<VerifyMagicWalletChargeCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IPaymentGatewayService _paymentGateway;
|
||||||
|
private readonly ILogger<VerifyMagicWalletChargeCommandHandler> _logger;
|
||||||
|
|
||||||
|
public VerifyMagicWalletChargeCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IPaymentGatewayService paymentGateway,
|
||||||
|
ILogger<VerifyMagicWalletChargeCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_paymentGateway = paymentGateway;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,6 +92,25 @@ public static class SystemConstants
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Magic Wallet Settings
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ضریب شارژ کیفپول جادویی — واریز × 2.5 = اعتبار
|
||||||
|
/// </summary>
|
||||||
|
public const decimal MagicWalletMultiplier = 2.5m;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سقف واریز در هر دور جادویی (ریال) — 100M تومان
|
||||||
|
/// </summary>
|
||||||
|
public const long MagicWalletMaxDeposit = 1_000_000_000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سقف اعتبار در هر دور جادویی (ریال) — 250M تومان
|
||||||
|
/// </summary>
|
||||||
|
public const long MagicWalletMaxCredit = 2_500_000_000;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Shop Settings
|
#region Shop Settings
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -134,6 +153,11 @@ public static class SystemConstants
|
|||||||
["System.MaintenanceMode"] = SystemMaintenanceMode,
|
["System.MaintenanceMode"] = SystemMaintenanceMode,
|
||||||
["System.EnableAuditLog"] = SystemEnableAuditLog,
|
["System.EnableAuditLog"] = SystemEnableAuditLog,
|
||||||
|
|
||||||
|
// Magic Wallet
|
||||||
|
["MagicWallet.Multiplier"] = MagicWalletMultiplier,
|
||||||
|
["MagicWallet.MaxDeposit"] = MagicWalletMaxDeposit,
|
||||||
|
["MagicWallet.MaxCredit"] = MagicWalletMaxCredit,
|
||||||
|
|
||||||
// Shop
|
// Shop
|
||||||
["Shop.VAT"] = ShopVAT,
|
["Shop.VAT"] = ShopVAT,
|
||||||
["Shop.VATEnabled"] = ShopVATEnabled
|
["Shop.VATEnabled"] = ShopVATEnabled
|
||||||
@@ -166,6 +190,11 @@ public static class SystemConstants
|
|||||||
("System.MaintenanceMode", SystemMaintenanceMode, "حالت تعمیر و نگهداری سیستم"),
|
("System.MaintenanceMode", SystemMaintenanceMode, "حالت تعمیر و نگهداری سیستم"),
|
||||||
("System.EnableAuditLog", SystemEnableAuditLog, "فعالسازی لاگ تغییرات"),
|
("System.EnableAuditLog", SystemEnableAuditLog, "فعالسازی لاگ تغییرات"),
|
||||||
|
|
||||||
|
// Magic Wallet
|
||||||
|
("MagicWallet.Multiplier", MagicWalletMultiplier, "ضریب شارژ کیفپول جادویی (×2.5)"),
|
||||||
|
("MagicWallet.MaxDeposit", MagicWalletMaxDeposit, "سقف واریز هر دور جادویی (ریال)"),
|
||||||
|
("MagicWallet.MaxCredit", MagicWalletMaxCredit, "سقف اعتبار هر دور جادویی (ریال)"),
|
||||||
|
|
||||||
// Shop
|
// Shop
|
||||||
("Shop.VAT", ShopVAT, "مالیات بر ارزش افزوده"),
|
("Shop.VAT", ShopVAT, "مالیات بر ارزش افزوده"),
|
||||||
("Shop.VATEnabled", ShopVATEnabled, "مالیات فعال است؟")
|
("Shop.VATEnabled", ShopVATEnabled, "مالیات فعال است؟")
|
||||||
|
|||||||
@@ -55,4 +55,9 @@ public class ClubMembership : BaseAuditableEntity
|
|||||||
/// ClubMembershipHistory Collection Navigation Reference
|
/// ClubMembershipHistory Collection Navigation Reference
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual ICollection<ClubMembershipHistory>? ClubMembershipHistories { get; set; }
|
public virtual ICollection<ClubMembershipHistory>? ClubMembershipHistories { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دورههای خرید پکیج — هر خرید پکیج یک Cycle جدید
|
||||||
|
/// </summary>
|
||||||
|
public virtual ICollection<ClubMembershipCycle>? Cycles { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
namespace CMSMicroservice.Domain.Entities.Club;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دوره خرید پکیج — هر بار خرید پکیج ۵۶M یک Cycle جدید ایجاد میشود.
|
||||||
|
/// برای حل مشکل overwrite شدن ClubMembership.ActivatedAt در محاسبه کمیسیون.
|
||||||
|
/// </summary>
|
||||||
|
public class ClubMembershipCycle : BaseAuditableEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User Navigation Property
|
||||||
|
/// </summary>
|
||||||
|
public virtual User User { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه عضویت باشگاه
|
||||||
|
/// </summary>
|
||||||
|
public long ClubMembershipId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ClubMembership Navigation Property
|
||||||
|
/// </summary>
|
||||||
|
public virtual ClubMembership ClubMembership { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شماره دور (1, 2, 3, ...)
|
||||||
|
/// </summary>
|
||||||
|
public int CycleNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تاریخ خرید پکیج — این فیلد برای محاسبه کمیسیون هفتگی استفاده میشود
|
||||||
|
/// (بهجای ClubMembership.ActivatedAt که overwrite میشد)
|
||||||
|
/// </summary>
|
||||||
|
public DateTime PackagePurchasedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تاریخ شروع حالت جادویی (وقتی Balance=0 شد)
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? MagicStartedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تاریخ پایان حالت جادویی
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? MagicCompletedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نحوه خرید پکیج در این دور
|
||||||
|
/// </summary>
|
||||||
|
public PackagePurchaseMethod PurchaseMethod { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// مبلغ پکیج (ریال)
|
||||||
|
/// </summary>
|
||||||
|
public long PackageAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیا این دور فعلی است؟ فقط یک رکورد true میباشد
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCurrentCycle { get; set; }
|
||||||
|
}
|
||||||
@@ -18,7 +18,36 @@ public class UserWallet : BaseAuditableEntity
|
|||||||
/// موجودی تخفیف - فقط برای خرید از فروشگاه باشگاه مشتریان
|
/// موجودی تخفیف - فقط برای خرید از فروشگاه باشگاه مشتریان
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long DiscountBalance { get; set; }
|
public long DiscountBalance { get; set; }
|
||||||
|
|
||||||
|
#region Magic Wallet
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حالت کیفپول — Normal: کمیسیون فعال | Magic: شارژ ×2.5 بدون کمیسیون
|
||||||
|
/// </summary>
|
||||||
|
public WalletMode WalletMode { get; set; } = WalletMode.Normal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// مجموع واریزی واقعی در دور جادویی فعلی (ریال) — سقف: MagicWalletMaxDeposit
|
||||||
|
/// </summary>
|
||||||
|
public long MagicTotalDeposited { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// مجموع اعتبار دادهشده در دور جادویی فعلی (ریال) — واریز × 2.5
|
||||||
|
/// </summary>
|
||||||
|
public long MagicTotalCredited { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// زمان شروع دور جادویی فعلی
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? MagicActivatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// زمان پایان دور جادویی فعلی
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? MagicCompletedAt { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
//UserWalletChangeLog Collection Navigation Reference
|
//UserWalletChangeLog Collection Navigation Reference
|
||||||
public virtual ICollection<UserWalletChangeLog> UserWalletChangeLogs { get; set; }
|
public virtual ICollection<UserWalletChangeLog> UserWalletChangeLogs { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,14 @@ public enum TransactionType
|
|||||||
/// خرید از فروشگاه تخفیف
|
/// خرید از فروشگاه تخفیف
|
||||||
/// </summary>
|
/// </summary>
|
||||||
DiscountShopPurchase = 13,
|
DiscountShopPurchase = 13,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// واریز از درگاه به کیفپول جادویی (مبلغ واقعی)
|
||||||
|
/// </summary>
|
||||||
|
MagicWalletDeposit = 14,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// بونوس داخلی کیفپول جادویی (مبلغ × 1.5)
|
||||||
|
/// </summary>
|
||||||
|
MagicWalletBonus = 15,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace CMSMicroservice.Domain.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حالت کیفپول — Normal: کمیسیون فعال | Magic: شارژ ×2.5 بدون کمیسیون
|
||||||
|
/// </summary>
|
||||||
|
public enum WalletMode
|
||||||
|
{
|
||||||
|
/// <summary>حالت عادی — کمیسیون و پورسانت فعال</summary>
|
||||||
|
Normal = 0,
|
||||||
|
|
||||||
|
/// <summary>حالت جادویی — شارژ ×2.5، بدون کمیسیون</summary>
|
||||||
|
Magic = 1
|
||||||
|
}
|
||||||
@@ -109,6 +109,7 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
|
|||||||
public DbSet<ClubFeature> ClubFeatures => Set<ClubFeature>();
|
public DbSet<ClubFeature> ClubFeatures => Set<ClubFeature>();
|
||||||
public DbSet<UserClubFeature> UserClubFeatures => Set<UserClubFeature>();
|
public DbSet<UserClubFeature> UserClubFeatures => Set<UserClubFeature>();
|
||||||
public DbSet<ClubMembershipHistory> ClubMembershipHistories => Set<ClubMembershipHistory>();
|
public DbSet<ClubMembershipHistory> ClubMembershipHistories => Set<ClubMembershipHistory>();
|
||||||
|
public DbSet<ClubMembershipCycle> ClubMembershipCycles => Set<ClubMembershipCycle>();
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
public DbSet<NetworkWeeklyBalance> NetworkWeeklyBalances => Set<NetworkWeeklyBalance>();
|
public DbSet<NetworkWeeklyBalance> NetworkWeeklyBalances => Set<NetworkWeeklyBalance>();
|
||||||
|
|||||||
+45
@@ -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<ClubMembershipCycle>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<ClubMembershipCycle> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -1,4 +1,5 @@
|
|||||||
using CMSMicroservice.Domain.Entities;
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
|
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
|
||||||
@@ -20,5 +21,18 @@ public class UserWalletConfiguration : IEntityTypeConfiguration<UserWallet>
|
|||||||
builder.Property(entity => entity.NetworkBalance).IsRequired(true);
|
builder.Property(entity => entity.NetworkBalance).IsRequired(true);
|
||||||
builder.Property(entity => entity.DiscountBalance).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);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-4
@@ -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
|
var activeClubMemberUserIds = await _context.ClubMemberships
|
||||||
.Where(c => c.IsActive)
|
.Where(c => c.IsActive && !magicModeUserIds.Contains(c.UserId))
|
||||||
.Select(c => c.UserId)
|
.Select(c => c.UserId)
|
||||||
.ToHashSetAsync(cancellationToken);
|
.ToHashSetAsync(cancellationToken);
|
||||||
|
|
||||||
@@ -364,10 +370,11 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy
|
|||||||
}
|
}
|
||||||
|
|
||||||
var count = 0;
|
var count = 0;
|
||||||
var membership = await _context.ClubMemberships
|
// ⚠️ از ClubMembershipCycle.PackagePurchasedAt استفاده میکنیم (نه ActivatedAt که overwrite میشد)
|
||||||
.FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsActive, cancellationToken);
|
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;
|
count = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>0.0.180</Version>
|
<Version>0.0.181</Version>
|
||||||
<DebugType>None</DebugType>
|
<DebugType>None</DebugType>
|
||||||
<DebugSymbols>False</DebugSymbols>
|
<DebugSymbols>False</DebugSymbols>
|
||||||
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
|
|
||||||
<Target Name="PushToFoursatNuget" AfterTargets="Pack" Condition="'$(CI)' != 'true'">
|
<Target Name="PushToFoursatNuget" AfterTargets="Pack" Condition="'$(CI)' != 'true'">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<NugetPackagePath>$(PackageOutputPath)/$(PackageId).$(Version).nupkg</NugetPackagePath>
|
<NugetPackagePath>/home/masoud/Apps/project/FourSat/CMS/src/CMSMicroservice.Protobuf/bin/Debug/$(PackageId).$(Version).nupkg</NugetPackagePath>
|
||||||
<PushCommand>dotnet nuget push "$(NugetPackagePath)" --source foursat-hosted --api-key admin:87zH26nbqT --skip-duplicate --configfile "$(MSBuildThisFileDirectory)../NuGet.config"</PushCommand>
|
<PushCommand>dotnet nuget push "$(NugetPackagePath)" --source foursat-hosted --api-key admin:87zH26nbqT --skip-duplicate --configfile "$(MSBuildThisFileDirectory)../NuGet.config"</PushCommand>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,20 @@ service UserWalletContract
|
|||||||
get: "/Customer/GetWithdrawalSettings"
|
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
|
message CreateNewUserWalletRequest
|
||||||
{
|
{
|
||||||
@@ -140,6 +154,7 @@ message GetCustomerWalletResponse
|
|||||||
int64 balance = 1;
|
int64 balance = 1;
|
||||||
int64 network_balance = 2;
|
int64 network_balance = 2;
|
||||||
int64 discount_balance = 3;
|
int64 discount_balance = 3;
|
||||||
|
int32 wallet_mode = 4; // 0=Normal, 1=Magic
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetCustomerWalletChangeLogRequest
|
message GetCustomerWalletChangeLogRequest
|
||||||
@@ -200,4 +215,29 @@ message CustomerWithdrawalModel
|
|||||||
message GetCustomerWithdrawalSettingsResponse
|
message GetCustomerWithdrawalSettingsResponse
|
||||||
{
|
{
|
||||||
int64 min_withdrawal_amount = 1;
|
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;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
|
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
|
||||||
|
using CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -143,4 +144,46 @@ public class PaymentCallbackController : ControllerBase
|
|||||||
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=error");
|
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Callback برای شارژ کیفپول جادویی — زرینپال بعد از پرداخت کاربر را اینجا برمیگرداند
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/api/wallet/verify-magic-charge")]
|
||||||
|
public async Task<IActionResult> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrderHistory;
|
|||||||
using CMSMicroservice.Application.OrderManagementCQ.Commands.UpdateOrderStatus;
|
using CMSMicroservice.Application.OrderManagementCQ.Commands.UpdateOrderStatus;
|
||||||
using CMSMicroservice.Application.OrderManagementCQ.Commands.CancelOrderByAdmin;
|
using CMSMicroservice.Application.OrderManagementCQ.Commands.CancelOrderByAdmin;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Common;
|
||||||
using CMSMicroservice.Domain.Entities;
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using CMSMicroservice.Domain.Entities.Club;
|
||||||
using CMSMicroservice.Domain.Entities.Order;
|
using CMSMicroservice.Domain.Entities.Order;
|
||||||
using CMSMicroservice.Domain.Enums;
|
using CMSMicroservice.Domain.Enums;
|
||||||
using AppModels = CMSMicroservice.Application.Common.Models;
|
using AppModels = CMSMicroservice.Application.Common.Models;
|
||||||
@@ -272,7 +274,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate amounts
|
// Calculate amounts
|
||||||
const decimal vatRate = 0.09m;
|
const decimal vatRate = 0.10m;
|
||||||
long baseAmount = cartItems.Sum(c => c.Product.Price * c.Count);
|
long baseAmount = cartItems.Sum(c => c.Product.Price * c.Count);
|
||||||
long vatAmount = (long)(baseAmount * vatRate);
|
long vatAmount = (long)(baseAmount * vatRate);
|
||||||
long totalAmount = baseAmount + vatAmount;
|
long totalAmount = baseAmount + vatAmount;
|
||||||
@@ -334,7 +336,54 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
|
|||||||
};
|
};
|
||||||
|
|
||||||
_context.UserWalletChangeLogs.Add(walletLog);
|
_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
|
// Create order
|
||||||
var order = new UserOrder
|
var order = new UserOrder
|
||||||
{
|
{
|
||||||
@@ -935,11 +984,11 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
|
|||||||
|
|
||||||
public override Task<GetVATRateResponse> GetVATRate(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context)
|
public override Task<GetVATRateResponse> GetVATRate(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
// VAT Rate for Iran: 9% (نرخ مالیات بر ارزش افزوده ایران)
|
// VAT Rate for Iran: 10% (نرخ مالیات بر ارزش افزوده ایران)
|
||||||
return Task.FromResult(new GetVATRateResponse
|
return Task.FromResult(new GetVATRateResponse
|
||||||
{
|
{
|
||||||
VatRate = 0.09,
|
VatRate = 0.10,
|
||||||
VatPercentage = 9,
|
VatPercentage = 10,
|
||||||
IsEnabled = true
|
IsEnabled = true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,17 @@ using CMSMicroservice.WebApi.Common.Services;
|
|||||||
using CMSMicroservice.Application.UserWalletCQ.Commands.CreateNewUserWallet;
|
using CMSMicroservice.Application.UserWalletCQ.Commands.CreateNewUserWallet;
|
||||||
using CMSMicroservice.Application.UserWalletCQ.Commands.UpdateUserWallet;
|
using CMSMicroservice.Application.UserWalletCQ.Commands.UpdateUserWallet;
|
||||||
using CMSMicroservice.Application.UserWalletCQ.Commands.DeleteUserWallet;
|
using CMSMicroservice.Application.UserWalletCQ.Commands.DeleteUserWallet;
|
||||||
|
using CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet;
|
||||||
using CMSMicroservice.Application.UserWalletCQ.Queries.GetUserWallet;
|
using CMSMicroservice.Application.UserWalletCQ.Queries.GetUserWallet;
|
||||||
using CMSMicroservice.Application.UserWalletCQ.Queries.GetAllUserWalletByFilter;
|
using CMSMicroservice.Application.UserWalletCQ.Queries.GetAllUserWalletByFilter;
|
||||||
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletChangeLog;
|
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletChangeLog;
|
||||||
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawals;
|
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawals;
|
||||||
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawalSettings;
|
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawalSettings;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Common;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
@@ -64,11 +68,15 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
|
|||||||
var walletQuery = new GetUserWalletQuery { Id = userId };
|
var walletQuery = new GetUserWalletQuery { Id = userId };
|
||||||
var wallet = await _sender.Send(walletQuery, context.CancellationToken);
|
var wallet = await _sender.Send(walletQuery, context.CancellationToken);
|
||||||
|
|
||||||
|
var walletEntity = await _context.UserWallets
|
||||||
|
.FirstOrDefaultAsync(w => w.UserId == userId, context.CancellationToken);
|
||||||
|
|
||||||
return new GetCustomerWalletResponse
|
return new GetCustomerWalletResponse
|
||||||
{
|
{
|
||||||
Balance = wallet.Balance,
|
Balance = wallet.Balance,
|
||||||
NetworkBalance = wallet.NetworkBalance,
|
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();
|
return new Google.Protobuf.WellKnownTypes.Empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============= Magic Wallet Methods =============
|
||||||
|
|
||||||
|
public override async Task<InitiateMagicChargeResponse> 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<GetMagicWalletStatusResponse> 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()
|
private long GetCurrentUserId()
|
||||||
{
|
{
|
||||||
if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0)
|
if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0)
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user