# 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 InitiatePaymentAsync( PaymentRequest request, CancellationToken cancellationToken = default); // تایید پرداخت (Callback) Task VerifyPaymentAsync( string refId, string verificationToken, CancellationToken cancellationToken = default); // برداشت/پرداخت به کاربر (Withdrawal) Task 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(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 {TERMINAL_ID} {USERNAME} {PASSWORD} {ORDER_ID} {AMOUNT_IN_RIALS} {yyyyMMdd} {HHmmss} {DESCRIPTION} {CALLBACK_URL} 0 ``` **Response**: ```xml {REF_ID} ``` #### bpVerifyRequest (Verify Payment) ```xml {TERMINAL_ID} {USERNAME} {PASSWORD} {ORDER_ID} {ORDER_ID} {REF_ID} ``` #### bpSettleRequest (Settle Payment) ```xml {TERMINAL_ID} {USERNAME} {PASSWORD} {ORDER_ID} {ORDER_ID} {REF_ID} ``` **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("UseRealPaymentGateway", false); if (useRealPaymentGateway) { var paymentProvider = configuration.GetValue("PaymentProvider", "BankMellat"); if (paymentProvider == "Daya") { services.AddHttpClient() .SetHandlerLifetime(TimeSpan.FromMinutes(5)); } else if (paymentProvider == "BankMellat") { services.AddHttpClient() .SetHandlerLifetime(TimeSpan.FromMinutes(5)); } else { throw new InvalidOperationException($"Invalid PaymentProvider: {paymentProvider}"); } } else { // Mock برای Development و Testing services.AddScoped(); } ``` --- ## 📝 Usage Examples ### Purchase Package (InitiatePaymentAsync) ```csharp // In Command Handler public class PurchaseGoldenPackageCommandHandler : IRequestHandler { private readonly IPaymentGatewayService _paymentGateway; public async Task 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 { 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 { 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>(); 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>(); 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> { private readonly HttpClient _client; public PaymentGatewayIntegrationTests(WebApplicationFactory 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(); 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