14 KiB
14 KiB
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)
توضیح جریان:
- کاربر محصول را در Frontend انتخاب میکند
- FrontOffice.BFF درخواست خرید را به PYMS Microservice میفرستد
- PYMS/Gateway با درگاه پرداخت (بانک) ارتباط برقرار میکند و پرداخت را انجام میدهد
- Gateway نتیجه پرداخت را به CMS Callback میفرستد
- CMS تراکنش را تایید و عملیات بعدی (فعالسازی، اضافه PV، Wallet) را انجام میدهد
- PYMS URL درگاه را برمیگرداند
- کاربر به درگاه ریدایرکت میشود و پرداخت میکند
- بعد از پرداخت، Callback به PYMS برمیگردد
- PYMS پرداخت را Verify میکند
- FrontOffice.BFF نتیجه را به CMS میفرستد
- 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:
using PYMSMicroservice.Protobuf.Protos.Transaction;
using Grpc.Core;
var client = new TransactionContract.TransactionContractClient(channel);
Available Methods:
1. PaymentRequest (شروع پرداخت)
// 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 (تایید پرداخت)
// 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): شناسه تراکنش در سیستم PYMSPaymentStatus(bool): وضعیت پرداخت (true = موفق، false = ناموفق)Message(string): پیام وضعیتRefId(string): شناسه مرجع از درگاه پرداختOrderId(string): شناسه سفارش که در PaymentRequest ارسال شدهVerificationStatusCode(int): کد وضعیت تایید
3. CreateNewTransaction (ثبت تراکنش جدید)
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 (بهروزرسانی تراکنش)
var request = new UpdateTransactionRequest
{
Id = transactionId,
PaymentStatus = true, // بعد از verify
RefId = "...",
VerificationStatusCode = 100,
VerificationStatusMessage = "تراکنش موفق"
};
await client.UpdateTransactionAsync(request);
5. GetTransaction (دریافت تراکنش)
var request = new GetTransactionRequest
{
Id = transactionId,
// یا
Authority = "AUTHORITY_FROM_CALLBACK"
};
var response = await client.GetTransactionAsync(request);
6. GetAllTransactionByFilter (لیست تراکنشها)
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
public enum CurrencyEnum
{
Irr = 0, // ریال
Irt = 1 // تومان
}
TransactionTypeEnum
public enum TransactionTypeEnum
{
Real = 0, // تراکنش واقعی
Sandbox = 1 // تراکنش تستی
}
💡 نکات مهم برای CMS
1. CMS فقط نتیجه را ثبت میکند
CMS نباید خودش با درگاه پرداخت ارتباط برقرار کند. این کار توسط PYMS Microservice انجام میشود.
2. Flow پیشنهادی برای Phase 9 (Club Discount Shop):
در FrontOffice.BFF:
// 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 (بعد از بازگشت از درگاه):
// 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:
// 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
✅ مزایای این معماری
- ✅ Separation of Concerns: CMS فقط روی business logic خودش تمرکز دارد
- ✅ Single Responsibility: PYMS مسئول پرداخت است، CMS مسئول ثبت نتیجه
- ✅ Easy Testing: میتوان PYMS را با Mock جایگزین کرد
- ✅ Scalability: هر microservice بهصورت مستقل scale میشود
- ✅ Maintainability: تغییرات در درگاه پرداخت فقط در PYMS انجام میشود
⚠️ نکات امنیتی
- همیشه Verify کنید: حتی اگر Status=OK باشد، حتماً PaymentVerification را صدا بزنید
- Callback را Validate کنید: مطمئن شوید request واقعاً از درگاه آمده (IP whitelisting)
- OrderId را Validate کنید: مطمئن شوید OrderId متعلق به همان کاربری است که لاگین کرده
- مبلغ را چک کنید: مبلغ پرداخت شده با مبلغ سفارش مطابقت داشته باشد
- Idempotency: از ثبت تکراری تراکنش جلوگیری کنید (با RefId)
📚 مثال کامل برای Phase 9
در فاز 9، باید:
- ✅ FrontOffice.BFF درخواست پرداخت را به PYMS بفرستد
- ✅ PYMS URL درگاه را برگرداند
- ✅ بعد از بازگشت، FrontOffice.BFF verify کند
- ✅ نتیجه را به 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 (فعالسازی)
- Entity:
- ✅ این سرویسها فقط برای مستندسازی و درک معماری نوشته شدند
- ✅ در عمل، PYMS Microservice مسئول ارتباط با درگاه است
- ✅ CMS فقط نتیجه را ثبت میکند