Files
docs/03-BACKEND/CMS/payment-architecture-pyms.md
T
masoodafar-web 119e870a26 feat: Complete overhaul of FourSat documentation structure and content
- 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
2025-12-04 17:32:31 +03:30

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)

توضیح جریان:

  1. کاربر محصول را در Frontend انتخاب می‌کند
  2. FrontOffice.BFF درخواست خرید را به PYMS Microservice می‌فرستد
  3. PYMS/Gateway با درگاه پرداخت (بانک) ارتباط برقرار می‌کند و پرداخت را انجام می‌دهد
  4. Gateway نتیجه پرداخت را به CMS Callback می‌فرستد
  5. CMS تراکنش را تایید و عملیات بعدی (فعال‌سازی، اضافه PV، Wallet) را انجام می‌دهد
  6. PYMS URL درگاه را برمی‌گرداند
  7. کاربر به درگاه ریدایرکت می‌شود و پرداخت می‌کند
  8. بعد از پرداخت، Callback به PYMS برمی‌گردد
  9. PYMS پرداخت را Verify می‌کند
  10. FrontOffice.BFF نتیجه را به CMS می‌فرستد
  11. 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): شناسه تراکنش در سیستم PYMS
  • PaymentStatus (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

مزایای این معماری

  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 فقط نتیجه را ثبت می‌کند