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
411 lines
14 KiB
Markdown
411 lines
14 KiB
Markdown
# 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 فقط نتیجه را ثبت میکند**
|