# Payment Architecture with PYMS Microservice **تاریخ**: 2024-12-02 **وضعیت**: Architecture Document **اولویت**: 🔴 بالا (اطلاعات مهم برای Phase 9) --- ## 📋 خلاصه **درگاه پرداخت** در این پروژه از طریق **مایکروسرویس PYMS** (`Afrino.PYMSMicroservice.Protobuf`) مدیریت می‌شود. **CMS Microservice** فقط **نتیجه نهایی پرداخت** را ثبت می‌کند و خودش درگاه پرداخت را پیاده‌سازی نمی‌کند. --- ## 🏗️ معماری کلی ``` [User Frontend] ↓ [FrontOffice.BFF] ← درخواست خرید از اینجا شروع می‌شود ↓ [PYMS Microservice] ← مدیریت درگاه پرداخت (Afrino.PYMSMicroservice.Protobuf) ↓ [Payment Gateway: در PYMS/Gateway - نه CMS] ↓ (Callback) [PYMS Microservice] ← تایید پرداخت ↓ [CMS Microservice] ← **فقط ثبت نتیجه** (Transaction با RefId) ``` ### توضیح جریان: 1. **کاربر** محصول را در Frontend انتخاب می‌کند 2. **FrontOffice.BFF** درخواست خرید را به **PYMS Microservice** می‌فرستد 3. **PYMS/Gateway** با درگاه پرداخت (بانک) ارتباط برقرار می‌کند و پرداخت را انجام می‌دهد 4. **Gateway** نتیجه پرداخت را به **CMS Callback** می‌فرستد 5. **CMS** تراکنش را تایید و عملیات بعدی (فعال‌سازی، اضافه PV، Wallet) را انجام می‌دهد 4. **PYMS** URL درگاه را برمی‌گرداند 5. کاربر به درگاه ریدایرکت می‌شود و پرداخت می‌کند 6. بعد از پرداخت، **Callback** به **PYMS** برمی‌گردد 7. **PYMS** پرداخت را Verify می‌کند 8. **FrontOffice.BFF** نتیجه را به **CMS** می‌فرستد 9. **CMS** Transaction را با RefId و وضعیت نهایی ثبت می‌کند --- ## 📦 Package: `Afrino.PYMSMicroservice.Protobuf` **Version**: 0.0.11 **Type**: gRPC Protobuf Client **Namespace**: `PYMSMicroservice.Protobuf.Protos.Transaction` ### Dependencies: - Google.Protobuf (3.23.3) - Grpc.Core.Api (2.54.0) - FluentValidation (11.2.2) - Google.Api.CommonProtos (2.10.0) --- ## 🔧 TransactionContract Service ### Client Class: ```csharp using PYMSMicroservice.Protobuf.Protos.Transaction; using Grpc.Core; var client = new TransactionContract.TransactionContractClient(channel); ``` ### Available Methods: #### 1. **PaymentRequest** (شروع پرداخت) ```csharp // Request var request = new PaymentRequestRequest { MerchantId = "YOUR_MERCHANT_ID", // شناسه فروشنده Amount = 100000, // مبلغ به ریال (یا تومان - بستگی به Currency) CallbackUrl = "https://yoursite.com/payment/callback", Description = "خرید بسته طلایی", Mobile = "09123456789", // اختیاری Email = "user@example.com", // اختیاری Currency = CurrencyEnum.Irt, // IRR (ریال) یا IRT (تومان) Type = TransactionTypeEnum.Real, // Real یا Sandbox OrderId = "ORDER_123456" // اختیاری - شناسه سفارش خودمان }; // Call var response = await client.PaymentRequestAsync(request); // Response Console.WriteLine(response.PaymentGWUrl); // مثال: "https://bpm.shaparak.ir/pgwchannel/startpay.mellat?RefId=123456" ``` **Response Fields**: - `PaymentGWUrl` (string): URL درگاه پرداخت که کاربر باید به آن ریدایرکت شود #### 2. **PaymentVerification** (تایید پرداخت) ```csharp // Request var request = new PaymentVerificationRequest { Authority = "AUTHORITY_FROM_CALLBACK", // Authority که از callback می‌آید Status = "OK" // Status که از callback می‌آید (OK/NOK) }; // Call var response = await client.PaymentVerificationAsync(request); // Response if (response.PaymentStatus) { Console.WriteLine($"پرداخت موفق!"); Console.WriteLine($"RefId: {response.RefId}"); Console.WriteLine($"OrderId: {response.OrderId}"); Console.WriteLine($"Message: {response.Message}"); Console.WriteLine($"VerificationStatusCode: {response.VerificationStatusCode}"); } else { Console.WriteLine($"پرداخت ناموفق: {response.Message}"); } ``` **Response Fields**: - `Id` (long): شناسه تراکنش در سیستم PYMS - `PaymentStatus` (bool): وضعیت پرداخت (true = موفق، false = ناموفق) - `Message` (string): پیام وضعیت - `RefId` (string): شناسه مرجع از درگاه پرداخت - `OrderId` (string): شناسه سفارش که در PaymentRequest ارسال شده - `VerificationStatusCode` (int): کد وضعیت تایید #### 3. **CreateNewTransaction** (ثبت تراکنش جدید) ```csharp var request = new CreateNewTransactionRequest { MerchantId = "...", Amount = 100000, CallbackUrl = "...", Description = "...", Currency = CurrencyEnum.Irt, PaymentStatus = false, // false در ابتدا Type = TransactionTypeEnum.Real }; var response = await client.CreateNewTransactionAsync(request); Console.WriteLine($"Transaction Id: {response.Id}"); ``` #### 4. **UpdateTransaction** (به‌روزرسانی تراکنش) ```csharp var request = new UpdateTransactionRequest { Id = transactionId, PaymentStatus = true, // بعد از verify RefId = "...", VerificationStatusCode = 100, VerificationStatusMessage = "تراکنش موفق" }; await client.UpdateTransactionAsync(request); ``` #### 5. **GetTransaction** (دریافت تراکنش) ```csharp var request = new GetTransactionRequest { Id = transactionId, // یا Authority = "AUTHORITY_FROM_CALLBACK" }; var response = await client.GetTransactionAsync(request); ``` #### 6. **GetAllTransactionByFilter** (لیست تراکنش‌ها) ```csharp var request = new GetAllTransactionByFilterRequest { PaginationState = new PaginationState { PageNumber = 1, PageSize = 10 }, Filter = new GetAllTransactionByFilterFilter { MerchantId = "...", PaymentStatus = true } }; var response = await client.GetAllTransactionByFilterAsync(request); // response.Models: لیست تراکنش‌ها // response.MetaData: اطلاعات صفحه‌بندی ``` --- ## 🔑 Enums ### CurrencyEnum ```csharp public enum CurrencyEnum { Irr = 0, // ریال Irt = 1 // تومان } ``` ### TransactionTypeEnum ```csharp public enum TransactionTypeEnum { Real = 0, // تراکنش واقعی Sandbox = 1 // تراکنش تستی } ``` --- ## 💡 نکات مهم برای CMS ### 1. **CMS فقط نتیجه را ثبت می‌کند** CMS نباید خودش با درگاه پرداخت ارتباط برقرار کند. این کار توسط **PYMS Microservice** انجام می‌شود. ### 2. **Flow پیشنهادی برای Phase 9 (Club Discount Shop)**: #### در FrontOffice.BFF: ```csharp // 1. کاربر محصول را انتخاب می‌کند var product = await cmsClient.GetProductAsync(productId); // 2. محاسبه تخفیف var userWallet = await cmsClient.GetUserWalletAsync(userId); var maxDiscountAmount = product.Price * (product.MaxDiscountPercent / 100); var actualDiscountAmount = Math.Min(userWallet.DiscountBalance, maxDiscountAmount); var gatewayAmount = product.Price - actualDiscountAmount; // 3. ثبت Order در CMS با وضعیت Pending var order = await cmsClient.CreateDiscountOrderAsync(new CreateDiscountOrderRequest { UserId = userId, ProductId = productId, TotalAmount = product.Price, DiscountAmount = actualDiscountAmount, GatewayAmount = gatewayAmount, Status = OrderStatus.Pending }); // 4. درخواست پرداخت از PYMS var paymentResponse = await pymsClient.PaymentRequestAsync(new PaymentRequestRequest { MerchantId = "YOUR_MERCHANT_ID", Amount = (long)gatewayAmount, // مبلغی که باید از درگاه پرداخت شود CallbackUrl = $"https://yoursite.com/payment/verify?orderId={order.Id}", Description = $"خرید {product.Title}", Currency = CurrencyEnum.Irt, Type = TransactionTypeEnum.Real, OrderId = order.Id.ToString() }); // 5. ریدایرکت به درگاه return Redirect(paymentResponse.PaymentGWUrl); ``` #### در Callback (بعد از بازگشت از درگاه): ```csharp // 1. دریافت Authority و Status از Query String var authority = Request.Query["Authority"]; var status = Request.Query["Status"]; var orderId = Request.Query["orderId"]; // 2. تایید پرداخت از PYMS var verifyResponse = await pymsClient.PaymentVerificationAsync(new PaymentVerificationRequest { Authority = authority, Status = status }); // 3. ثبت نتیجه در CMS if (verifyResponse.PaymentStatus) { // 3.1. کسر DiscountBalance await cmsClient.DeductDiscountBalanceAsync(new DeductDiscountBalanceRequest { UserId = userId, Amount = order.DiscountAmount, Description = $"خرید محصول {product.Title}", RefId = verifyResponse.RefId }); // 3.2. ثبت Transaction در CMS await cmsClient.CreateTransactionAsync(new CreateTransactionRequest { UserId = userId, Type = TransactionType.DiscountPurchase, Amount = order.TotalAmount, DiscountAmount = order.DiscountAmount, GatewayAmount = order.GatewayAmount, RefId = verifyResponse.RefId, Status = TransactionStatus.Completed, Description = $"خرید {product.Title}" }); // 3.3. تغییر وضعیت Order به Completed await cmsClient.CompleteDiscountOrderAsync(new CompleteDiscountOrderRequest { OrderId = orderId, RefId = verifyResponse.RefId }); return View("PaymentSuccess"); } else { // 3.4. تغییر وضعیت Order به Failed await cmsClient.FailDiscountOrderAsync(new FailDiscountOrderRequest { OrderId = orderId, ErrorMessage = verifyResponse.Message }); return View("PaymentFailed", verifyResponse.Message); } ``` ### 3. **Entity های مورد نیاز در CMS**: ```csharp // Domain/Entities/DiscountOrder.cs public class DiscountOrder { public long Id { get; set; } public long UserId { get; set; } public long ProductId { get; set; } public decimal TotalAmount { get; set; } public decimal DiscountAmount { get; set; } // مبلغ از DiscountBalance public decimal GatewayAmount { get; set; } // مبلغ از درگاه public OrderStatus Status { get; set; } // Pending/Completed/Failed public string? RefId { get; set; } // RefId از PYMS public string? ErrorMessage { get; set; } public DateTime CreatedAt { get; set; } public DateTime? CompletedAt { get; set; } // Navigation public User User { get; set; } public Product Product { get; set; } } // Domain/Enums/OrderStatus.cs public enum OrderStatus { Pending = 0, // در انتظار پرداخت Completed = 1, // پرداخت موفق Failed = 2 // پرداخت ناموفق } ``` ### 4. **Commands مورد نیاز در CMS**: - `CreateDiscountOrderCommand`: ثبت سفارش اولیه - `CompleteDiscountOrderCommand`: تکمیل سفارش بعد از پرداخت موفق - `FailDiscountOrderCommand`: شکست سفارش - `DeductDiscountBalanceCommand`: کسر از DiscountBalance --- ## ✅ مزایای این معماری 1. ✅ **Separation of Concerns**: CMS فقط روی business logic خودش تمرکز دارد 2. ✅ **Single Responsibility**: PYMS مسئول پرداخت است، CMS مسئول ثبت نتیجه 3. ✅ **Easy Testing**: می‌توان PYMS را با Mock جایگزین کرد 4. ✅ **Scalability**: هر microservice به‌صورت مستقل scale می‌شود 5. ✅ **Maintainability**: تغییرات در درگاه پرداخت فقط در PYMS انجام می‌شود --- ## ⚠️ نکات امنیتی 1. **همیشه Verify کنید**: حتی اگر Status=OK باشد، حتماً PaymentVerification را صدا بزنید 2. **Callback را Validate کنید**: مطمئن شوید request واقعاً از درگاه آمده (IP whitelisting) 3. **OrderId را Validate کنید**: مطمئن شوید OrderId متعلق به همان کاربری است که لاگین کرده 4. **مبلغ را چک کنید**: مبلغ پرداخت شده با مبلغ سفارش مطابقت داشته باشد 5. **Idempotency**: از ثبت تکراری تراکنش جلوگیری کنید (با RefId) --- ## 📚 مثال کامل برای Phase 9 در فاز 9، باید: 1. ✅ **FrontOffice.BFF** درخواست پرداخت را به **PYMS** بفرستد 2. ✅ **PYMS** URL درگاه را برگرداند 3. ✅ بعد از بازگشت، **FrontOffice.BFF** verify کند 4. ✅ نتیجه را به **CMS** بفرستد تا: - DiscountBalance کسر شود - Transaction ثبت شود - Order تکمیل شود --- **نتیجه‌گیری**: - ✅ **Payment Gateway Service** (فقط DayaPaymentService برای Payout) **فقط برای پرداخت به کاربران است** - ✅ **Transaction System در CMS** برای دریافت نتیجه پرداخت از Gateway و ادامه عملیات: - Entity: `Transaction` (ReferenceId, Amount, Status, Gateway) - Commands: `CreateTransaction`, `VerifyTransaction` (Callback), `RefundTransaction` - Queries: `GetTransactions`, `GetUserTransactions` - جریان: User → Gateway (پرداخت) → Callback به CMS → CMS (فعال‌سازی) - ✅ این سرویس‌ها فقط برای **مستندسازی** و **درک معماری** نوشته شدند - ✅ در عمل، **PYMS Microservice** مسئول ارتباط با درگاه است - ✅ **CMS فقط نتیجه را ثبت می‌کند**