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

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

---

## 📋 فهرست

1. [خلاصه سیستم](#خلاصه-سیستم)
2. [Business Requirements](#business-requirements)
3. [معماری سیستم](#معماری-سیستم)
4. [Implementation Details](#implementation-details)
5. [Club Membership Contract System](#club-membership-contract-system)
6. [API Endpoints](#api-endpoints)
7. [Flow Diagram](#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:
```csharp
public enum PackagePurchaseMethod
{
    None = 0,           // هنوز خرید نکرده
    DirectPurchase = 1, // پرداخت مستقیم
    DayaLoan = 2        // اعتبار دایا
}
```

### ContractType Enum:
```csharp
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**
   ```csharp
   // 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**
   ```csharp
   // 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:
```protobuf
// 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**
   ```csharp
   // 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:**
   ```csharp
   // 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**
   ```csharp
   // 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:**
   ```csharp
   // 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:
```protobuf
// 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

   ```csharp
   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
   - نمایش نتیجه (موفق/ناموفق)

   ```csharp
   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:

```mermaid
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:
- [x] InitiateBasePackagePaymentCommand
- [x] InitiateBasePackagePaymentCommandValidator
- [x] InitiateBasePackagePaymentCommandHandler
- [x] VerifyBasePackagePaymentCommand
- [x] VerifyBasePackagePaymentCommandValidator
- [x] VerifyBasePackagePaymentCommandHandler
- [x] Proto messages و RPCs
- [x] PackageService implementation
- [x] Mapster mappings

### BFF:
- [x] InitiateBasePackagePaymentCommand
- [x] InitiateBasePackagePaymentCommandValidator
- [x] InitiateBasePackagePaymentCommandHandler
- [x] VerifyBasePackagePaymentCommand
- [x] VerifyBasePackagePaymentCommandValidator
- [x] VerifyBasePackagePaymentCommandHandler
- [x] Proto messages و RPCs
- [x] PackageService implementation
- [x] Mapster mappings
- [x] CurrentUserService integration

### Frontend:
- [x] Bottom Sheet UI برای انتخاب روش پرداخت
- [x] DirectPayment method
- [x] PaymentCallback page
- [x] UserAuthInfo.UserId
- [x] AuthService extract UserId
- [x] Navigation to payment gateway
- [x] Display payment result

### Testing:
- [ ] Test پرداخت موفق
- [ ] Test پرداخت ناموفق
- [ ] Test لغو پرداخت توسط کاربر
- [ ] Test خرید مجدد (باید خطا دهد)
- [ ] Test شارژ کیف پول
- [ ] Test فعالسازی لینک دعوت

---

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