feat: Complete overhaul of FourSat documentation structure and content
- Added FINAL-STATUS.md detailing project completion and key metrics - Created QUICK-REFERENCE.md for quick access to essential documents - Updated README.md with project overview and quick start guide - Established STRUCTURE.md outlining the final documentation structure - Organized and archived old files, ensuring a clean and efficient directory - Enhanced documentation quality with comprehensive metrics and checklists
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user