Files
docs/01-BUSINESS/base-package-payment-system.md
T
masoodafar-web 002e99f6bf Implement Persian Date Conversion and Enhance User Network Information Service
- Added PersianDateTimeService for converting Gregorian dates to Persian format in the BackOffice frontend.
- Updated multiple frontend pages (Dashboard, UserPayouts, WorkerControl, UserNetworkInfo) to utilize the new Persian date service.
- Enhanced GetUserNetworkPositionDto with 28+ new fields for comprehensive user network data.
- Updated GetUserNetworkPositionQueryHandler to include new methods for calculating network statistics.
- Modified Protobuf messages to accommodate the new fields, increasing from 14 to 42.
- Refined week number calculation algorithm to ensure consistency across C# and SQL implementations.
- Created new CSV and Excel files for binary plan calculations.
- Ensured all changes are tested and validated for accuracy and performance.
2025-12-20 06:15:59 +03:30

20 KiB

Base Package Payment System - سیستم پرداخت پکیج پایه

تاریخ ایجاد: 2024-12-16
تاریخ آخرین به‌روزرسانی: 2024-12-16
وضعیت: پیاده‌سازی شده
اولویت: 🔴 بسیار بالا


📋 فهرست

  1. خلاصه سیستم
  2. Business Requirements
  3. معماری سیستم
  4. Implementation Details
  5. Club Membership Contract System
  6. API Endpoints
  7. Flow Diagram
  8. نکات مهم

🎯 خلاصه سیستم

سیستم پرداخت پکیج پایه امکان پرداخت 56 میلیون تومان را برای کاربران فراهم می‌کند تا بتوانند:

  1. کیف پول خود را شارژ کنند (Balance + DiscountBalance)
  2. امضای قرارداد باشگاه مشتریان (گام الزامی بعد از پرداخت)
  3. فعالسازی لینک دعوت (Referral Link) - تنها بعد از امضای قرارداد
  4. دسترسی کامل به امکانات باشگاه مشتریان

دو روش پرداخت:

  1. پرداخت مستقیم (Direct Payment) - از طریق درگاه بانکی (زرین‌پال)
  2. اعتبار الماسی دایا (Daya Loan) - از طریق سایت دایا

📊 Business Requirements

شرایط نمایش لینک دعوت:

CanShowReferralLink = HasPurchasedPackage && IsClubMemberActive
  • HasPurchasedPackage: کاربر پکیج پایه را خریداری کرده (PackagePurchaseMethod != None)
  • IsClubMemberActive: قرارداد باشگاه مشتریان امضا شده (ClubMembership.IsActive = true)

⚠️ نکته مهم: پرداخت پکیج به تنهایی کافی نیست! کاربر باید قرارداد باشگاه مشتریان را نیز امضا کند.

مقدار پکیج:

  • مبلغ: 56,000,000 تومان
  • شارژ Balance: 56,000,000 تومان
  • شارژ DiscountBalance: 56,000,000 تومان

PackagePurchaseMethod Enum:

public enum PackagePurchaseMethod
{
    None = 0,           // هنوز خرید نکرده
    DirectPurchase = 1, // پرداخت مستقیم
    DayaLoan = 2        // اعتبار دایا
}

ContractType Enum:

public enum ContractType
{
    Main = 0,           // قرارداد ثبت‌نام اولیه
    ClubMembership = 1, // قرارداد باشگاه مشتریان
}

🏗️ معماری سیستم

Architecture Pattern:

Frontend (Blazor)
    ↓
BFF (Backend For Frontend)
    ↓ ↘
CMS     PYMS (Payment Gateway)

Layer Responsibilities:

1️⃣ Frontend (Blazor)

  • نمایش UI برای انتخاب روش پرداخت
  • فراخوانی BFF برای شروع پرداخت
  • مدیریت Callback از درگاه
  • نمایش نتیجه پرداخت
  • Modal غیرقابل بسته شدن برای امضای قرارداد باشگاه (جدید )

2️⃣ BFF (Middle Layer)

  • InitiateBasePackagePayment: هماهنگی بین CMS و PYMS

    • فراخوانی CMS برای ثبت Transaction + Order
    • فراخوانی PYMS برای دریافت URL درگاه
    • برگرداندن URL به Frontend
  • VerifyBasePackagePayment: تأیید پرداخت

    • فراخوانی PYMS برای Verify
    • فراخوانی CMS برای شارژ یا Reject
  • RequestClubContractOtp: ارسال OTP برای امضای قرارداد (جدید )

  • AcceptClubMembershipContract: امضای قرارداد و فعالسازی باشگاه (جدید )

3️⃣ CMS (Core Business)

  • InitiateBasePackagePayment: ثبت Transaction + Order با Pending
  • VerifyBasePackagePayment: شارژ کیف پول یا Reject بر اساس نتیجه
  • AcceptClubMembershipContract: ثبت UserContract و فعالسازی ClubMembership (جدید )

4️⃣ PYMS (Payment Gateway Service)

  • PaymentRequest: دریافت URL درگاه زرین‌پال
  • PaymentVerification: تأیید پرداخت از بانک

💻 Implementation Details

CMS Layer

Commands:

  1. InitiateBasePackagePaymentCommand

    // Input
    public record InitiateBasePackagePaymentCommand
    {
        public long UserId { get; init; }
    }
    
    // Output
    public class InitiateBasePackagePaymentResponseDto
    {
        public bool Success { get; set; }
        public string Message { get; set; }
        public long OrderId { get; set; }
        public long TransactionId { get; set; }
        public long Amount { get; set; } // 56,000,000
    }
    

    Handler Logic:

    • بررسی عدم خرید قبلی: user.PackagePurchaseMethod == None
    • بررسی عدم Order Pending قبلی
    • ایجاد Transaction با PaymentStatus.Pending
    • ایجاد UserOrder با PackageId=4, PaymentStatus.Pending
    • Return OrderId + TransactionId
  2. VerifyBasePackagePaymentCommand

    // Input
    public record VerifyBasePackagePaymentCommand
    {
        public long OrderId { get; init; }
        public long TransactionId { get; init; }
        public bool PaymentSuccess { get; init; } // از BFF می‌آید
        public string? RefId { get; init; }
        public string? Message { get; init; }
    }
    
    // Output
    public class VerifyBasePackagePaymentResponseDto
    {
        public bool Success { get; set; }
        public string Message { get; set; }
        public long OrderId { get; set; }
        public long TransactionId { get; set; }
        public string? ReferenceCode { get; set; }
        public long WalletBalance { get; set; }
        public long DiscountBalance { get; set; }
    }
    

    Handler Logic (Success):

    • شارژ wallet.Balance += 56,000,000
    • شارژ wallet.DiscountBalance += 56,000,000
    • ثبت Transaction با PaymentStatus.Success
    • ثبت UserWalletChangeLog (Balance + Discount)
    • Update Order: PaymentStatus.Success, PaymentMethod.IPG
    • Update User: PackagePurchaseMethod.DirectPurchase

    Handler Logic (Failed):

    • Update Transaction: PaymentStatus.Reject
    • Update Order: PaymentStatus.Reject

Proto Definition:

// package.proto
service PackageContract {
    rpc InitiateBasePackagePayment(InitiateBasePackagePaymentRequest) 
        returns (InitiateBasePackagePaymentResponse);
    
    rpc VerifyBasePackagePayment(VerifyBasePackagePaymentRequest) 
        returns (VerifyBasePackagePaymentResponse);
}

message InitiateBasePackagePaymentRequest {
    int64 user_id = 1;
}

message InitiateBasePackagePaymentResponse {
    bool success = 1;
    string message = 2;
    int64 order_id = 3;
    int64 transaction_id = 4;
    int64 amount = 5;
}

message VerifyBasePackagePaymentRequest {
    int64 order_id = 1;
    int64 transaction_id = 2;
    bool payment_success = 3;
    google.protobuf.StringValue ref_id = 4;
    google.protobuf.StringValue message = 5;
}

message VerifyBasePackagePaymentResponse {
    bool success = 1;
    string message = 2;
    int64 order_id = 3;
    int64 transaction_id = 4;
    google.protobuf.StringValue reference_code = 5;
    int64 wallet_balance = 6;
    int64 discount_balance = 7;
}

Files Created/Modified:

CMS/src/CMSMicroservice.Application/PackageCQ/Commands/
├── InitiateBasePackagePayment/
│   ├── InitiateBasePackagePaymentCommand.cs
│   ├── InitiateBasePackagePaymentCommandValidator.cs
│   └── InitiateBasePackagePaymentCommandHandler.cs
└── VerifyBasePackagePayment/
    ├── VerifyBasePackagePaymentCommand.cs
    ├── VerifyBasePackagePaymentCommandValidator.cs
    └── VerifyBasePackagePaymentCommandHandler.cs

CMS/src/CMSMicroservice.Protobuf/Protos/
└── package.proto (updated)

CMS/src/CMSMicroservice.WebApi/
├── Services/PackageService.cs (updated)
└── Common/Mappings/PackageProfile.cs (updated)

BFF Layer

Commands:

  1. InitiateBasePackagePaymentCommand

    // Input (UserId از CurrentUserService گرفته می‌شود)
    public record InitiateBasePackagePaymentCommand
    {
        public string CallbackUrl { get; init; }
    }
    
    // Output
    public class InitiateBasePackagePaymentResponseDto
    {
        public bool Success { get; set; }
        public string Message { get; set; }
        public long OrderId { get; set; }
        public long TransactionId { get; set; }
        public long Amount { get; set; }
        public string PaymentGatewayUrl { get; set; }
        public string Authority { get; set; }
    }
    

    Handler Logic:

    // 1. فراخوانی CMS
    var cmsResponse = await _context.Package.InitiateBasePackagePaymentAsync(
        new InitiateBasePackagePaymentRequest {
            UserId = _currentUserService.UserId.Value
        });
    
    // 2. فراخوانی PYMS
    var paymentResponse = await _context.ZarinTransactions.PaymentRequestAsync(
        new PaymentRequestRequest {
            MerchantId = "...",
            Amount = cmsResponse.Amount * 10, // تبدیل به ریال
            CallbackUrl = $"{request.CallbackUrl}?orderId={...}&transactionId={...}",
            Description = "پرداخت پکیج پایه",
            Currency = CurrencyEnum.Irr,
            Type = TransactionTypeEnum.Real
        });
    
    // 3. Return URL + Authority
    return new InitiateBasePackagePaymentResponseDto {
        PaymentGatewayUrl = paymentResponse.PaymentGWUrl,
        Authority = ExtractAuthorityFromUrl(paymentResponse.PaymentGWUrl),
        ...
    };
    
  2. VerifyBasePackagePaymentCommand

    // Input
    public record VerifyBasePackagePaymentCommand
    {
        public long OrderId { get; init; }
        public long TransactionId { get; init; }
        public string Authority { get; init; }
        public string Status { get; init; } // OK یا NOK
    }
    

    Handler Logic:

    // 1. بررسی Status
    if (request.Status != "OK") {
        await NotifyCmsPaymentFailed(...);
        return Failed;
    }
    
    // 2. Verify از PYMS
    var verifyResponse = await _context.ZarinTransactions
        .PaymentVerificationAsync(...);
    
    // 3. فراخوانی CMS
    if (verifyResponse.PaymentStatus) {
        var cmsResponse = await _context.Package.VerifyBasePackagePaymentAsync(
            new VerifyBasePackagePaymentRequest {
                OrderId = request.OrderId,
                TransactionId = request.TransactionId,
                PaymentSuccess = true,
                RefId = verifyResponse.RefId,
                Message = verifyResponse.Message
            });
        return Success;
    } else {
        await NotifyCmsPaymentFailed(...);
        return Failed;
    }
    

Proto Definition:

// package.proto
service PackageContract {
    rpc InitiateBasePackagePayment(InitiateBasePackagePaymentRequest) 
        returns (InitiateBasePackagePaymentResponse) {
        option (google.api.http) = {
            post: "/InitiateBasePackagePayment"
            body: "*"
        };
    };
    
    rpc VerifyBasePackagePayment(VerifyBasePackagePaymentRequest) 
        returns (VerifyBasePackagePaymentResponse) {
        option (google.api.http) = {
            post: "/VerifyBasePackagePayment"
            body: "*"
        };
    };
}

message InitiateBasePackagePaymentRequest {
    string callback_url = 1;
    // UserId از JWT token گرفته می‌شود
}

message InitiateBasePackagePaymentResponse {
    bool success = 1;
    string message = 2;
    int64 order_id = 3;
    int64 transaction_id = 4;
    int64 amount = 5;
    string payment_gateway_url = 6;
    string authority = 7;
}

message VerifyBasePackagePaymentRequest {
    int64 order_id = 1;
    int64 transaction_id = 2;
    string authority = 3;
    string status = 4;
}

message VerifyBasePackagePaymentResponse {
    bool success = 1;
    string message = 2;
    int64 order_id = 3;
    int64 transaction_id = 4;
    google.protobuf.StringValue ref_id = 5;
    int64 wallet_balance = 6;
    int64 discount_balance = 7;
}

Files Created/Modified:

FrontOffice.BFF/src/FrontOffice.BFF.Application/PackageCQ/Commands/
├── InitiateBasePackagePayment/
│   ├── InitiateBasePackagePaymentCommand.cs
│   ├── InitiateBasePackagePaymentCommandValidator.cs
│   └── InitiateBasePackagePaymentCommandHandler.cs
└── VerifyBasePackagePayment/
    ├── VerifyBasePackagePaymentCommand.cs
    ├── VerifyBasePackagePaymentCommandValidator.cs
    └── VerifyBasePackagePaymentCommandHandler.cs

FrontOffice.BFF/src/Protobufs/FrontOffice.BFF.Package.Protobuf/Protos/
└── package.proto (updated)

FrontOffice.BFF/src/FrontOffice.BFF.WebApi/
├── Services/PackageService.cs (updated)
└── Common/Mappings/PackageProfile.cs (updated)

FrontOffice.BFF/src/FrontOffice.BFF.Domain/
└── FrontOffice.BFF.Domain.csproj (updated - added CMS Proto reference)

Frontend Layer

Pages:

  1. Profile/Index.razor.cs

    • نمایش دکمه "خرید پکیج پایه"
    • Bottom Sheet با دو گزینه: پرداخت مستقیم / اعتبار الماسی
    • فراخوانی BFF.InitiateBasePackagePayment
    private async Task DirectPayment()
    {
        var callbackUrl = $"{Navigation.BaseUri}profile/payment-callback";
    
        var response = await PackageContract.InitiateBasePackagePaymentAsync(
            new InitiateBasePackagePaymentRequest {
                CallbackUrl = callbackUrl
            });
    
        if (response.Success) {
            Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true);
        }
    }
    
  2. Profile/PaymentCallback.razor

    • دریافت Query Parameters: orderId, transactionId, Authority, Status
    • فراخوانی BFF.VerifyBasePackagePayment
    • نمایش نتیجه (موفق/ناموفق)
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender) {
            var response = await PackageContract.VerifyBasePackagePaymentAsync(
                new VerifyBasePackagePaymentRequest {
                    OrderId = OrderId,
                    TransactionId = TransactionId,
                    Authority = Authority,
                    Status = Status
                });
    
            // نمایش نتیجه
        }
    }
    

Files Created/Modified:

FrontOffice/src/FrontOffice.Main/Pages/Profile/
├── Index.razor.cs (updated)
└── PaymentCallback.razor (new)

FrontOffice/src/FrontOffice.Main/Utilities/
├── UserAuthInfo.cs (updated - added UserId)
└── AuthService.cs (updated - extract UserId from JWT)

FrontOffice/src/FrontOffice.Main/
└── FrontOffice.Main.csproj (updated - added BFF Package Proto reference)

🔌 API Endpoints

BFF Endpoints (gRPC-Web + HTTP):

POST /InitiateBasePackagePayment
Body: {
  "callback_url": "https://example.com/profile/payment-callback"
}

Response: {
  "success": true,
  "message": "...",
  "order_id": 123,
  "transaction_id": 456,
  "amount": 56000000,
  "payment_gateway_url": "https://www.zarinpal.com/pg/StartPay/...",
  "authority": "A00000000000000000000000000123456"
}
POST /VerifyBasePackagePayment
Body: {
  "order_id": 123,
  "transaction_id": 456,
  "authority": "A00000000000000000000000000123456",
  "status": "OK"
}

Response: {
  "success": true,
  "message": "پرداخت با موفقیت تایید شد",
  "order_id": 123,
  "transaction_id": 456,
  "ref_id": "789",
  "wallet_balance": 56000000,
  "discount_balance": 56000000
}

📊 Flow Diagram

Complete Payment Flow:

sequenceDiagram
    participant User as کاربر
    participant FE as Frontend
    participant BFF as BFF
    participant CMS as CMS
    participant PYMS as PYMS
    participant Bank as درگاه بانک

    User->>FE: کلیک "پرداخت مستقیم"
    FE->>BFF: InitiateBasePackagePayment(CallbackUrl)
    BFF->>BFF: استخراج UserId از JWT
    BFF->>CMS: InitiateBasePackagePayment(UserId)
    CMS->>CMS: ثبت Transaction (Pending)
    CMS->>CMS: ثبت Order (Pending)
    CMS-->>BFF: OrderId, TransactionId, Amount
    
    BFF->>PYMS: PaymentRequest(Amount, Callback)
    PYMS-->>BFF: PaymentGWUrl, Authority
    BFF-->>FE: PaymentGWUrl, OrderId, TransactionId
    
    FE->>Bank: Redirect to PaymentGWUrl
    User->>Bank: پرداخت
    Bank-->>FE: Redirect to Callback?Authority=...&Status=OK
    
    FE->>BFF: VerifyBasePackagePayment(OrderId, TransactionId, Authority, Status)
    BFF->>PYMS: PaymentVerification(Authority)
    PYMS-->>BFF: PaymentStatus, RefId
    
    alt پرداخت موفق
        BFF->>CMS: VerifyBasePackagePayment(PaymentSuccess=true, RefId)
        CMS->>CMS: شارژ Balance (56M)
        CMS->>CMS: شارژ DiscountBalance (56M)
        CMS->>CMS: ثبت Transaction (Success)
        CMS->>CMS: ثبت WalletChangeLog
        CMS->>CMS: Update Order (Success)
        CMS->>CMS: Update User.PackagePurchaseMethod
        CMS-->>BFF: Success, WalletBalance, DiscountBalance
        BFF-->>FE: Success
        FE-->>User: نمایش پیام موفقیت + موجودی
    else پرداخت ناموفق
        BFF->>CMS: VerifyBasePackagePayment(PaymentSuccess=false)
        CMS->>CMS: Update Transaction (Reject)
        CMS->>CMS: Update Order (Reject)
        CMS-->>BFF: Failed
        BFF-->>FE: Failed
        FE-->>User: نمایش پیام خطا
    end

⚠️ نکات مهم

Security:

  1. UserId از JWT گرفته می‌شود نه از Request - امنیت بالاتر
  2. Validation در هر لایه انجام می‌شود
  3. Transaction Idempotency - چک می‌شود که Order Pending قبلی وجود نداشته باشد

Business Logic:

  1. کاربر فقط یک بار می‌تواند پکیج پایه بخرد
  2. شارژ هم‌زمان Balance و DiscountBalance انجام می‌شود
  3. PackagePurchaseMethod بعد از پرداخت موفق به DirectPurchase تغییر می‌کند
  4. برای فعالسازی لینک دعوت، باید هم پکیج خریداری شود هم باشگاه فعال شود

Error Handling:

  1. اگر CMS خطا برگرداند، به درگاه نمی‌رویم
  2. اگر PYMS URL ندهد، Transaction در CMS باقی می‌ماند (Pending)
  3. اگر Callback با Status=NOK بیاید، مستقیماً Reject می‌شود
  4. اگر Verification ناموفق باشد، Transaction و Order به Reject تغییر می‌کند

Project References:

برای development، از Project Reference استفاده می‌شود:

  • BFF → CMS.Protobuf (Project Reference)
  • Frontend → BFF.Package.Protobuf (Project Reference)

برای production، باید به NuGet Package تبدیل شوند.


Checklist پیاده‌سازی

CMS:

  • InitiateBasePackagePaymentCommand
  • InitiateBasePackagePaymentCommandValidator
  • InitiateBasePackagePaymentCommandHandler
  • VerifyBasePackagePaymentCommand
  • VerifyBasePackagePaymentCommandValidator
  • VerifyBasePackagePaymentCommandHandler
  • Proto messages و RPCs
  • PackageService implementation
  • Mapster mappings

BFF:

  • InitiateBasePackagePaymentCommand
  • InitiateBasePackagePaymentCommandValidator
  • InitiateBasePackagePaymentCommandHandler
  • VerifyBasePackagePaymentCommand
  • VerifyBasePackagePaymentCommandValidator
  • VerifyBasePackagePaymentCommandHandler
  • Proto messages و RPCs
  • PackageService implementation
  • Mapster mappings
  • CurrentUserService integration

Frontend:

  • Bottom Sheet UI برای انتخاب روش پرداخت
  • DirectPayment method
  • PaymentCallback page
  • UserAuthInfo.UserId
  • AuthService extract UserId
  • Navigation to payment gateway
  • Display payment result

Testing:

  • Test پرداخت موفق
  • Test پرداخت ناموفق
  • Test لغو پرداخت توسط کاربر
  • Test خرید مجدد (باید خطا دهد)
  • Test شارژ کیف پول
  • Test فعالسازی لینک دعوت

تاریخ آخرین به‌روزرسانی: 2024-12-16
نگارنده: Development Team