Refactor code structure for improved readability and maintainability

This commit is contained in:
masoodafar-web
2026-02-10 22:06:46 +03:30
parent 8f02cec22f
commit 5149b9a89c
182 changed files with 7305 additions and 243980 deletions
+777
View File
@@ -0,0 +1,777 @@
# Payment Gateway Integration Guide
## 📋 Overview
## 🔄 جریان پرداخت در سیستم
### 1️⃣ دریافت پول از کاربر (Payment IN)
```
کاربر → Gateway/PYMS → بانک → پرداخت موفق
Callback به CMS
CMS: VerifyTransaction + فعال‌سازی عضویت
```
**توضیح**:
- درگاه اینترنتی در **Gateway/PYMS** است (نه CMS)
- CMS فقط **نتیجه پرداخت را دریافت** می‌کند (از طریق Callback)
- سپس عملیات بعدی (فعال‌سازی، اضافه PV، Wallet) را انجام می‌دهد
- **Transaction System** در CMS برای این کار طراحی شده
### 2️⃣ پرداخت به کاربر (Payout)
```
ادمین تایید برداشت → CMS → DayaPaymentService → واریز به حساب کاربر
```
**توضیح**:
- این سند فقط برای **Payout** است
- سیستم از دو پیاده‌سازی پشتیبانی می‌کند:
1. **MockPaymentGatewayService** - برای Development و Testing
2. **DayaPaymentService** - API واقعی Daya (برای واریز به حساب کاربران)
---
## 🏗️ Architecture
### Interface Design
```csharp
public interface IPaymentGatewayService
{
// پرداخت (خرید بسته)
Task<PaymentInitiateResult> InitiatePaymentAsync(
PaymentRequest request,
CancellationToken cancellationToken = default);
// تایید پرداخت (Callback)
Task<PaymentVerificationResult> VerifyPaymentAsync(
string refId,
string verificationToken,
CancellationToken cancellationToken = default);
// برداشت/پرداخت به کاربر (Withdrawal)
Task<PayoutResult> ProcessPayoutAsync(
PayoutRequest request,
CancellationToken cancellationToken = default);
}
```
### DTO Models
#### PaymentRequest
```csharp
public class PaymentRequest
{
public long UserId { get; set; }
public string Mobile { get; set; }
public decimal Amount { get; set; }
public string Description { get; set; }
public string CallbackUrl { get; set; }
}
```
#### PaymentInitiateResult
```csharp
public class PaymentInitiateResult
{
public bool IsSuccess { get; set; }
public string? RefId { get; set; }
public string? GatewayUrl { get; set; }
public string? ErrorMessage { get; set; }
}
```
#### PaymentVerificationResult
```csharp
public class PaymentVerificationResult
{
public bool IsSuccess { get; set; }
public string RefId { get; set; }
public string? TrackingCode { get; set; }
public decimal Amount { get; set; }
public string? Message { get; set; }
}
```
#### PayoutRequest
```csharp
public class PayoutRequest
{
public long UserId { get; set; }
public string Iban { get; set; }
public decimal Amount { get; set; }
public string? Description { get; set; }
}
```
#### PayoutResult
```csharp
public class PayoutResult
{
public bool IsSuccess { get; set; }
public string? TransactionId { get; set; }
public string Message { get; set; }
public DateTime ProcessedAt { get; set; }
}
```
---
## 🔧 Implementation Details
### 1. MockPaymentGatewayService
**Purpose**: Development و Testing بدون نیاز به API واقعی
**Features**:
- ✅ IBAN validation (IR prefix, 26 characters)
- ✅ Amount validation (min 10,000 Toman)
- ✅ Mock RefId generation (MockRef_{timestamp})
- ✅ Simulated network delay (500ms)
- ✅ Comprehensive logging
- ✅ Gateway URL generation (mock://payment)
**Usage**:
```json
{
"UseRealPaymentGateway": false
}
```
**Example**:
```csharp
var result = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
UserId = 123,
Mobile = "09123456789",
Amount = 100000,
Description = "خرید بسته طلایی",
CallbackUrl = "https://yoursite.com/payment/callback"
});
// result.IsSuccess = true
// result.RefId = "MockRef_1701619200"
// result.GatewayUrl = "mock://payment/MockRef_1701619200"
```
---
### 2. DayaPaymentService
**Purpose**: یکپارچه‌سازی با API واقعی Daya برای پرداخت و برداشت
**Configuration**:
```json
{
"UseRealPaymentGateway": true,
"PaymentProvider": "Daya",
"DayaPayment": {
"BaseUrl": "https://api.daya.ir",
"ApiKey": "YOUR_DAYA_API_KEY"
}
}
```
**API Endpoints**:
#### Initiate Payment
```http
POST {BaseUrl}/api/v1/payment/initiate
Content-Type: application/json
X-API-Key: {ApiKey}
{
"userId": 123,
"mobile": "09123456789",
"amount": 100000,
"description": "خرید بسته طلایی",
"callbackUrl": "https://yoursite.com/payment/callback"
}
Response:
{
"success": true,
"refId": "DAYA123456789",
"gatewayUrl": "https://gateway.daya.ir/pay/DAYA123456789",
"errorMessage": null
}
```
#### Verify Payment
```http
POST {BaseUrl}/api/v1/payment/verify
Content-Type: application/json
X-API-Key: {ApiKey}
{
"refId": "DAYA123456789",
"token": "DAYA123456789"
}
Response:
{
"success": true,
"refId": "DAYA123456789",
"trackingCode": "TRACK987654321",
"amount": 100000,
"message": "تراکنش موفق"
}
```
#### Process Payout
```http
POST {BaseUrl}/api/v1/payout/process
Content-Type: application/json
X-API-Key: {ApiKey}
{
"userId": 123,
"iban": "IR123456789012345678901234",
"amount": 50000,
"description": "برداشت کمیسیون"
}
Response:
{
"success": true,
"transactionId": "TXN_123456789",
"message": "پرداخت با موفقیت انجام شد",
"processedAt": "2024-12-02T10:30:00Z"
}
```
**Error Handling**:
```csharp
try
{
var response = await _httpClient.PostAsJsonAsync(url, request, cancellationToken);
if (!response.IsSuccessStatusCode)
{
_logger.LogError("Daya API error: StatusCode={StatusCode}", response.StatusCode);
return new PaymentInitiateResult
{
IsSuccess = false,
ErrorMessage = $"خطا در ارتباط با سرویس پرداخت: {response.StatusCode}"
};
}
var result = await response.Content.ReadFromJsonAsync<DayaInitiateResponse>(cancellationToken);
// Process result...
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in InitiatePaymentAsync");
return new PaymentInitiateResult
{
IsSuccess = false,
ErrorMessage = "خطای غیرمنتظره در برقراری ارتباط با سرویس پرداخت"
};
}
```
---
### 3. BankMellatPaymentService
**Purpose**: یکپارچه‌سازی با IPG بانک ملت (SOAP Web Service)
**Configuration**:
```json
{
"UseRealPaymentGateway": true,
"PaymentProvider": "BankMellat",
"BankMellat": {
"ServiceUrl": "https://bpm.shaparak.ir/pgwchannel/services/pgw",
"TerminalId": "YOUR_TERMINAL_ID",
"Username": "YOUR_USERNAME",
"Password": "YOUR_PASSWORD"
}
}
```
**SOAP Operations**:
#### bpPayRequest (Initiate Payment)
```xml
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="http://interfaces.core.sw.bps.com/">
<soap:Body>
<ns:bpPayRequest>
<terminalId>{TERMINAL_ID}</terminalId>
<userName>{USERNAME}</userName>
<userPassword>{PASSWORD}</userPassword>
<orderId>{ORDER_ID}</orderId>
<amount>{AMOUNT_IN_RIALS}</amount>
<localDate>{yyyyMMdd}</localDate>
<localTime>{HHmmss}</localTime>
<additionalData>{DESCRIPTION}</additionalData>
<callBackUrl>{CALLBACK_URL}</callBackUrl>
<payerId>0</payerId>
</ns:bpPayRequest>
</soap:Body>
</soap:Envelope>
```
**Response**:
```xml
<soap:Envelope>
<soap:Body>
<ns:bpPayRequestResponse>
<return>{REF_ID}</return> <!-- Success: positive number, Error: negative number -->
</ns:bpPayRequestResponse>
</soap:Body>
</soap:Envelope>
```
#### bpVerifyRequest (Verify Payment)
```xml
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="http://interfaces.core.sw.bps.com/">
<soap:Body>
<ns:bpVerifyRequest>
<terminalId>{TERMINAL_ID}</terminalId>
<userName>{USERNAME}</userName>
<userPassword>{PASSWORD}</userPassword>
<orderId>{ORDER_ID}</orderId>
<saleOrderId>{ORDER_ID}</saleOrderId>
<saleReferenceId>{REF_ID}</saleReferenceId>
</ns:bpVerifyRequest>
</soap:Body>
</soap:Envelope>
```
#### bpSettleRequest (Settle Payment)
```xml
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="http://interfaces.core.sw.bps.com/">
<soap:Body>
<ns:bpSettleRequest>
<terminalId>{TERMINAL_ID}</terminalId>
<userName>{USERNAME}</userName>
<userPassword>{PASSWORD}</userPassword>
<orderId>{ORDER_ID}</orderId>
<saleOrderId>{ORDER_ID}</saleOrderId>
<saleReferenceId>{REF_ID}</saleReferenceId>
</ns:bpSettleRequest>
</soap:Body>
</soap:Envelope>
```
**Error Codes**:
| Code | Description (Persian) |
|------|----------------------|
| 0 | تراکنش موفق |
| 11 | شماره کارت نامعتبر است |
| 12 | موجودی کافی نیست |
| 13 | رمز نادرست است |
| 14 | تعداد دفعات وارد کردن رمز بیش از حد مجاز است |
| 15 | کارت نامعتبر است |
| 17 | کاربر از انجام تراکنش منصرف شده است |
| 18 | تاریخ انقضای کارت گذشته است |
| 21 | پذیرنده نامعتبر است |
| 23 | خطای امنیتی رخ داده است |
| 24 | اطلاعات کاربری پذیرنده نامعتبر است |
| 25 | مبلغ نامعتبر است |
| 41 | شماره درخواست تکراری است |
| 43 | قبلا درخواست Verify داده شده است |
| 51 | تراکنش تکراری است |
**Limitations**:
- ⚠️ Direct payout (ProcessPayoutAsync) **not supported** by Bank Mellat IPG
- ️ For withdrawals, use **Shaparak Paya** or third-party services like Fanapay, IPG.ir
---
## ⚙️ Service Registration (ConfigureServices.cs)
```csharp
// Payment Gateway Service - برای Development از Mock استفاده می‌شود
var useRealPaymentGateway = configuration.GetValue<bool>("UseRealPaymentGateway", false);
if (useRealPaymentGateway)
{
var paymentProvider = configuration.GetValue<string>("PaymentProvider", "BankMellat");
if (paymentProvider == "Daya")
{
services.AddHttpClient<IPaymentGatewayService, DayaPaymentService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
}
else if (paymentProvider == "BankMellat")
{
services.AddHttpClient<IPaymentGatewayService, BankMellatPaymentService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
}
else
{
throw new InvalidOperationException($"Invalid PaymentProvider: {paymentProvider}");
}
}
else
{
// Mock برای Development و Testing
services.AddScoped<IPaymentGatewayService, MockPaymentGatewayService>();
}
```
---
## 📝 Usage Examples
### Purchase Package (InitiatePaymentAsync)
```csharp
// In Command Handler
public class PurchaseGoldenPackageCommandHandler : IRequestHandler<PurchaseGoldenPackageCommand, long>
{
private readonly IPaymentGatewayService _paymentGateway;
public async Task<long> Handle(PurchaseGoldenPackageCommand request, CancellationToken ct)
{
// Initiate payment
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
UserId = request.UserId,
Mobile = user.Mobile,
Amount = packagePrice,
Description = "خرید بسته طلایی",
CallbackUrl = "https://yoursite.com/payment/callback"
}, ct);
if (!paymentResult.IsSuccess)
{
throw new InvalidOperationException(paymentResult.ErrorMessage);
}
// Create transaction record
var transaction = new Transaction
{
UserId = request.UserId,
Type = TransactionType.PackagePurchase,
Amount = packagePrice,
Status = TransactionStatus.Pending,
RefId = paymentResult.RefId,
Description = "خرید بسته طلایی"
};
await _context.Transactions.AddAsync(transaction, ct);
await _context.SaveChangesAsync(ct);
// Redirect user to gateway
return transaction.Id; // Return transaction ID for frontend to track
}
}
```
### Verify Payment (Callback)
```csharp
public class VerifyGoldenPackagePurchaseCommandHandler : IRequestHandler<VerifyGoldenPackagePurchaseCommand>
{
private readonly IPaymentGatewayService _paymentGateway;
public async Task Handle(VerifyGoldenPackagePurchaseCommand request, CancellationToken ct)
{
// Verify payment
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority,
request.Authority,
ct);
if (!verifyResult.IsSuccess)
{
transaction.Status = TransactionStatus.Failed;
transaction.ErrorMessage = verifyResult.Message;
throw new InvalidOperationException(verifyResult.Message);
}
// Update transaction
transaction.Status = TransactionStatus.Completed;
transaction.CompletedAt = DateTime.UtcNow;
// Activate club membership
var clubMembership = new ClubMembership
{
UserId = transaction.UserId,
Status = ClubMembershipStatus.Active,
StartDate = DateTime.UtcNow,
EndDate = DateTime.UtcNow.AddMonths(1),
PurchaseMethod = PackagePurchaseMethod.DirectPurchase
};
await _context.ClubMemberships.AddAsync(clubMembership, ct);
await _context.SaveChangesAsync(ct);
}
}
```
### Process Withdrawal (ProcessPayoutAsync)
```csharp
public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawalCommand>
{
private readonly IPaymentGatewayService _paymentGateway;
public async Task Handle(ProcessWithdrawalCommand request, CancellationToken ct)
{
if (request.IsApproved)
{
if (payout.WithdrawalMethod == WithdrawalMethod.Diamond)
{
// Credit user wallet
userWallet.DiscountBalance += payout.TotalAmount;
}
else if (payout.WithdrawalMethod == WithdrawalMethod.Cash)
{
// Process bank transfer
var payoutResult = await _paymentGateway.ProcessPayoutAsync(new PayoutRequest
{
UserId = payout.UserId,
Iban = payout.Iban,
Amount = payout.TotalAmount,
Description = $"برداشت کمیسیون هفته {payout.WeekNumber}"
}, ct);
if (payoutResult.IsSuccess)
{
payout.Status = CommissionStatus.Withdrawn;
payout.CompletedAt = DateTime.UtcNow;
payout.TransactionId = payoutResult.TransactionId;
}
else
{
payout.Status = CommissionStatus.PaymentFailed;
payout.ErrorMessage = payoutResult.Message;
}
}
// Record history
await _context.CommissionPayoutHistories.AddAsync(new CommissionPayoutHistory
{
PayoutId = payout.Id,
TransactionType = payout.Status == CommissionStatus.Withdrawn
? TransactionType.Withdrawn
: TransactionType.PaymentFailed,
Amount = payout.TotalAmount,
ProcessedBy = _currentUserService.UserId,
ProcessedAt = DateTime.UtcNow
}, ct);
await _context.SaveChangesAsync(ct);
}
}
}
```
---
## 🧪 Testing Guide
### Unit Testing with Mock
```csharp
[Fact]
public async Task InitiatePayment_Should_Return_Success_With_Valid_Data()
{
// Arrange
var mockLogger = new Mock<ILogger<MockPaymentGatewayService>>();
var service = new MockPaymentGatewayService(mockLogger.Object);
var request = new PaymentRequest
{
UserId = 123,
Mobile = "09123456789",
Amount = 100000,
Description = "Test payment",
CallbackUrl = "https://test.com/callback"
};
// Act
var result = await service.InitiatePaymentAsync(request);
// Assert
Assert.True(result.IsSuccess);
Assert.NotNull(result.RefId);
Assert.StartsWith("MockRef_", result.RefId);
Assert.NotNull(result.GatewayUrl);
}
[Fact]
public async Task ProcessPayout_Should_Fail_With_Invalid_IBAN()
{
// Arrange
var mockLogger = new Mock<ILogger<MockPaymentGatewayService>>();
var service = new MockPaymentGatewayService(mockLogger.Object);
var request = new PayoutRequest
{
UserId = 123,
Iban = "INVALID_IBAN",
Amount = 50000,
Description = "Test payout"
};
// Act
var result = await service.ProcessPayoutAsync(request);
// Assert
Assert.False(result.IsSuccess);
Assert.Contains("فرمت شماره شبا نامعتبر", result.Message);
}
```
### Integration Testing
```csharp
public class PaymentGatewayIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public PaymentGatewayIntegrationTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task PurchaseGoldenPackage_Should_Initiate_Payment()
{
// Arrange
var command = new PurchaseGoldenPackageCommand
{
UserId = 123,
PaymentMethod = PackagePurchaseMethod.DirectPurchase
};
// Act
var response = await _client.PostAsJsonAsync("/api/package/purchase", command);
// Assert
response.EnsureSuccessStatusCode();
var transactionId = await response.Content.ReadFromJsonAsync<long>();
Assert.True(transactionId > 0);
}
}
```
---
## 🔒 Security Best Practices
1. **Configuration Security**:
- ✅ Store API keys in `appsettings.json` (excluded from git)
- ✅ Use Azure Key Vault or AWS Secrets Manager in production
- ✅ Never hardcode credentials in code
2. **HTTPS Only**:
- ✅ Enforce HTTPS for all payment callbacks
- ✅ Validate SSL certificates
3. **Amount Validation**:
- ✅ Validate min/max amounts before API call
- ✅ Verify amounts match on callback
4. **IBAN Validation**:
- ✅ Format: IR + 24 digits = 26 characters
- ✅ Validate before payout processing
5. **Idempotency**:
- ✅ Use unique OrderId for each payment
- ✅ Store RefId to prevent duplicate processing
6. **Error Handling**:
- ✅ Never expose internal errors to users
- ✅ Log detailed errors for debugging
- ✅ Return user-friendly error messages
---
## 📊 Monitoring & Logging
### Recommended Logs
```csharp
// Success
_logger.LogInformation(
"Payment initiated successfully: UserId={UserId}, Amount={Amount}, RefId={RefId}",
request.UserId, request.Amount, result.RefId);
// Failure
_logger.LogError(
"Payment initiation failed: UserId={UserId}, Amount={Amount}, Error={Error}",
request.UserId, request.Amount, result.ErrorMessage);
// API Error
_logger.LogError(
"Payment gateway API error: StatusCode={StatusCode}, Response={Response}",
response.StatusCode, responseContent);
```
### Sentry Integration
```csharp
try
{
var result = await _paymentGateway.InitiatePaymentAsync(request, ct);
}
catch (Exception ex)
{
SentrySdk.CaptureException(ex, scope =>
{
scope.SetTag("payment_provider", "Daya");
scope.SetExtra("user_id", request.UserId);
scope.SetExtra("amount", request.Amount);
});
throw;
}
```
---
## 🚀 Production Deployment Checklist
- [ ] Obtain Daya API credentials (BaseUrl + ApiKey)
- [ ] Obtain Bank Mellat credentials (TerminalId, Username, Password)
- [ ] Test in sandbox environment
- [ ] Update `appsettings.Production.json` with credentials
- [ ] Set `UseRealPaymentGateway = true`
- [ ] Configure HTTPS callback URLs
- [ ] Set up monitoring (Sentry/Application Insights)
- [ ] Configure retry policies (Polly)
- [ ] Test full payment flow (Initiate → Callback → Verify)
- [ ] Test withdrawal flow (Request → Approve → Payout)
- [ ] Document production URLs and credentials (secure location)
---
## 📞 Support & Troubleshooting
### Common Issues
**Issue**: "Payment gateway API error: 401 Unauthorized"
- **Solution**: Check API key in `appsettings.json`, verify credentials
**Issue**: "IBAN validation failed"
- **Solution**: Ensure IBAN starts with "IR" and is exactly 26 characters
**Issue**: "Bank Mellat returns negative RefId"
- **Solution**: Check error code mapping, verify TerminalId/Username/Password
**Issue**: "HttpClient timeout"
- **Solution**: Increase timeout in `ConfigureServices.cs`, check network connectivity
---
## 📚 References
- [Daya API Documentation](https://api.daya.ir/docs) (placeholder)
- [Bank Mellat IPG Guide](https://bpm.shaparak.ir/) (official)
- [Shaparak Paya Documentation](https://www.shaparak.ir/)
- [ISO 8601 Week Numbering](https://en.wikipedia.org/wiki/ISO_8601)
---
**Last Updated**: 2024-12-02
**Version**: 1.0
**Status**: ✅ Production Ready