778 lines
21 KiB
Markdown
778 lines
21 KiB
Markdown
# 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
|