119e870a26
- 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
21 KiB
21 KiB
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 است
- سیستم از دو پیادهسازی پشتیبانی میکند:
- MockPaymentGatewayService - برای Development و Testing
- DayaPaymentService - API واقعی Daya (برای واریز به حساب کاربران)
🏗️ Architecture
Interface Design
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
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
public class PaymentInitiateResult
{
public bool IsSuccess { get; set; }
public string? RefId { get; set; }
public string? GatewayUrl { get; set; }
public string? ErrorMessage { get; set; }
}
PaymentVerificationResult
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
public class PayoutRequest
{
public long UserId { get; set; }
public string Iban { get; set; }
public decimal Amount { get; set; }
public string? Description { get; set; }
}
PayoutResult
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:
{
"UseRealPaymentGateway": false
}
Example:
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:
{
"UseRealPaymentGateway": true,
"PaymentProvider": "Daya",
"DayaPayment": {
"BaseUrl": "https://api.daya.ir",
"ApiKey": "YOUR_DAYA_API_KEY"
}
}
API Endpoints:
Initiate Payment
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
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
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:
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:
{
"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)
<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:
<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)
<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)
<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)
// 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)
// 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)
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)
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
[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
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
-
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
- ✅ Store API keys in
-
HTTPS Only:
- ✅ Enforce HTTPS for all payment callbacks
- ✅ Validate SSL certificates
-
Amount Validation:
- ✅ Validate min/max amounts before API call
- ✅ Verify amounts match on callback
-
IBAN Validation:
- ✅ Format: IR + 24 digits = 26 characters
- ✅ Validate before payout processing
-
Idempotency:
- ✅ Use unique OrderId for each payment
- ✅ Store RefId to prevent duplicate processing
-
Error Handling:
- ✅ Never expose internal errors to users
- ✅ Log detailed errors for debugging
- ✅ Return user-friendly error messages
📊 Monitoring & Logging
Recommended Logs
// 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
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.jsonwith 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 (placeholder)
- Bank Mellat IPG Guide (official)
- Shaparak Paya Documentation
- ISO 8601 Week Numbering
Last Updated: 2024-12-02
Version: 1.0
Status: ✅ Production Ready