# Club Membership Contract System - سیستم قرارداد باشگاه مشتریان

**تاریخ ایجاد:** 2024-12-16  
**وضعیت:** ✅ پیاده‌سازی شده  
**اولویت:** 🔴 بسیار بالا  
**مرتبط با:** [base-package-payment-system.md](./base-package-payment-system.md)

---

## 📋 فهرست

1. [خلاصه سیستم](#خلاصه-سیستم)
2. [Business Requirements](#business-requirements)
3. [Complete Flow](#complete-flow)
4. [CMS Layer](#cms-layer)
5. [BFF Layer](#bff-layer)
6. [Frontend Layer](#frontend-layer)
7. [OTP SMS Format](#otp-sms-format)
8. [Token Refresh Pattern](#token-refresh-pattern)
9. [Testing Checklist](#testing-checklist)

---

## 🎯 خلاصه سیستم

سیستم قرارداد باشگاه مشتریان یک **مدال غیرقابل بسته شدن** است که بعد از پرداخت موفق پکیج پایه، کاربر را ملزم به **امضای قرارداد** می‌کند تا بتواند:
1. عضویت باشگاه مشتریان فعال شود
2. لینک دعوت (Referral Link) نمایش داده شود
3. به امکانات کامل باشگاه مشتریان دسترسی داشته باشد

### ویژگی‌های کلیدی:
- ✅ Modal **غیرقابل بسته شدن** (کاربر نمی‌تواند Escape یا Click بیرون را استفاده کند)
- ✅ **OTP Verification** برای امنیت بالاتر
- ✅ **Automatic Token Refresh** بعد از امضای موفق
- ✅ ثبت قرارداد در دیتابیس با **HTML content** و **SignGuid**

---

## 📊 Business Requirements

### شرایط نمایش Modal:
```csharp
if (HasPurchasedPackage && !IsClubMemberActive)
{
    // نمایش Modal قرارداد
}
```

- **HasPurchasedPackage**: `PackagePurchaseMethod != None` (پرداخت موفق انجام شده)
- **IsClubMemberActive**: `ClubMembership.IsActive = true` (قرارداد امضا شده)

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

### OTP Configuration:
- **Purpose**: `signClubContract`
- **Expiry**: 120 seconds (2 minutes)
- **Code Length**: 6 digits
- **SMS Provider**: Kavenegar

### ClubMembership Activation Values:
```csharp
ClubMembership {
    IsActive = true,
    ActivatedAt = DateTime.Now,
    InitialContribution = 56_000_000,  // مبلغ اولیه
    GiftValue = 25_200_000,            // ارزش هدیه (45% از 56M)
    PurchaseMethod = user.PackagePurchaseMethod
}
```

---

## 🔄 Complete Flow

```mermaid
sequenceDiagram
    participant User
    participant Frontend
    participant BFF
    participant CMS
    participant SMS as Kavenegar

    Note over User,SMS: 1️⃣ Payment Successful (قبلاً انجام شده)
    
    User->>Frontend: ورود به صفحه Profile
    Frontend->>Frontend: CheckAndShowClubContractModal()
    
    alt HasPurchasedPackage && !IsClubMemberActive
        Frontend->>User: نمایش Modal غیرقابل بسته شدن
        User->>User: مطالعه قرارداد (HTML Content)
        
        Note over User,SMS: 2️⃣ Request OTP
        User->>Frontend: کلیک "درخواست کد تایید"
        Frontend->>BFF: RequestClubContractOtp(SignGuid)
        BFF->>CMS: CreateNewOtpToken(mobile, "signClubContract")
        CMS-->>BFF: OTP Code (6 digits)
        BFF->>SMS: Send SMS(mobile, code, signGuid, fullName)
        SMS-->>User: پیامک با کد OTP
        BFF-->>Frontend: Success
        Frontend->>Frontend: شروع Timer (120 ثانیه)
        
        Note over User,SMS: 3️⃣ Accept Contract
        User->>Frontend: وارد کردن OTP Code
        Frontend->>BFF: AcceptClubMembershipContract(OtpCode, SignGuid, ContractHtml)
        BFF->>CMS: AcceptClubMembershipContract(UserId, OtpCode, SignGuid, ContractHtml)
        
        CMS->>CMS: VerifyOtpAsync(mobile, "signClubContract", OtpCode)
        alt OTP Invalid
            CMS-->>BFF: Error: "کد وارد شده اشتباه است"
            BFF-->>Frontend: Error
            Frontend->>User: پیام خطا
        else OTP Valid
            CMS->>CMS: ثبت Contract (اگر وجود نداشته باشد)
            CMS->>CMS: ثبت UserContract (SignGuid, ContractHtml)
            CMS->>CMS: فعالسازی ClubMembership (IsActive=true)
            CMS-->>BFF: Success
            
            Note over User,SMS: 4️⃣ Token Refresh
            BFF->>CMS: GetJwtToken(UserId)
            CMS-->>BFF: New JWT Token
            BFF-->>Frontend: Success + NewToken
            
            Frontend->>Frontend: ذخیره Token در localStorage
            Frontend->>Frontend: بستن Modal و Refresh صفحه
            Frontend->>User: نمایش پیام موفقیت + لینک دعوت
        end
    end
```

---

## 💻 CMS Layer

### 📂 File Structure:
```
CMS/
  src/CMSMicroservice.Domain/
    Enums/
      ContractType.cs                 ✅ Modified
    
  src/CMSMicroservice.Application/
    ClubMemberships/
      Commands/
        AcceptClubMembershipContract/
          AcceptClubMembershipContractCommand.cs           ✅ Created
          AcceptClubMembershipContractCommandValidator.cs  ✅ Created
          AcceptClubMembershipContractCommandHandler.cs    ✅ Created
    
    Profiles/
      ClubFeatureProfile.cs           ✅ Modified
  
  src/CMSMicroservice.Protobuf/
    Protos/
      clubmembership.proto            ✅ Modified
  
  src/CMSMicroservice.WebApi/
    Services/
      ClubMembershipService.cs        ✅ Modified
```

---

### 1️⃣ ContractType.cs

```csharp
namespace CMSMicroservice.Domain.Enums;

/// <summary>
/// تعیین نوع قرارداد
/// </summary>
public enum ContractType
{
    /// <summary>
    /// قرارداد ثبت‌نام اولیه
    /// </summary>
    Main = 0,

    /// <summary>
    /// قرارداد باشگاه مشتریان
    /// </summary>
    ClubMembership = 1,
}
```

**تغییرات:** `CMS = 1` → `ClubMembership = 1`

---

### 2️⃣ AcceptClubMembershipContractCommand.cs

```csharp
namespace CMSMicroservice.Application.ClubMemberships.Commands.AcceptClubMembershipContract;

/// <summary>
/// Command برای پذیرش و امضای قرارداد باشگاه مشتریان
/// </summary>
public record AcceptClubMembershipContractCommand
{
    /// <summary>
    /// شناسه کاربر
    /// </summary>
    public required long UserId { get; init; }

    /// <summary>
    /// کد OTP ارسال شده به کاربر (6 رقمی)
    /// </summary>
    public required string OtpCode { get; init; }

    /// <summary>
    /// شناسه یکتای امضاء (GUID)
    /// </summary>
    public required string SignGuid { get; init; }

    /// <summary>
    /// محتوای HTML قرارداد برای ذخیره
    /// </summary>
    public required string ContractHtml { get; init; }
}
```

---

### 3️⃣ AcceptClubMembershipContractCommandValidator.cs

```csharp
using FluentValidation;

namespace CMSMicroservice.Application.ClubMemberships.Commands.AcceptClubMembershipContract;

public class AcceptClubMembershipContractCommandValidator 
    : AbstractValidator<AcceptClubMembershipContractCommand>
{
    public AcceptClubMembershipContractCommandValidator()
    {
        RuleFor(x => x.UserId)
            .GreaterThan(0)
            .WithMessage("شناسه کاربر نامعتبر است");

        RuleFor(x => x.OtpCode)
            .NotEmpty()
            .WithMessage("کد تایید الزامی است")
            .Length(6)
            .WithMessage("کد تایید باید 6 رقمی باشد")
            .Matches(@"^\d{6}$")
            .WithMessage("کد تایید فقط باید شامل اعداد باشد");

        RuleFor(x => x.SignGuid)
            .NotEmpty()
            .WithMessage("شناسه امضاء الزامی است")
            .Must(guid => Guid.TryParse(guid, out _))
            .WithMessage("شناسه امضاء نامعتبر است");

        RuleFor(x => x.ContractHtml)
            .NotEmpty()
            .WithMessage("محتوای قرارداد الزامی است")
            .MinimumLength(100)
            .WithMessage("محتوای قرارداد نامعتبر است");
    }
}
```

---

### 4️⃣ AcceptClubMembershipContractCommandHandler.cs

```csharp
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace CMSMicroservice.Application.ClubMemberships.Commands.AcceptClubMembershipContract;

public class AcceptClubMembershipContractCommandHandler 
    : IRequestHandler<AcceptClubMembershipContractCommand, bool>
{
    private readonly IApplicationDbContext _context;

    public AcceptClubMembershipContractCommandHandler(IApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<bool> Handle(
        AcceptClubMembershipContractCommand request, 
        CancellationToken cancellationToken)
    {
        // 1️⃣ دریافت کاربر با ClubMembership
        var user = await _context.Users
            .Include(u => u.ClubMembership)
            .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);

        if (user == null)
            throw new Exception("کاربر یافت نشد");

        // 2️⃣ بررسی پیش‌نیازها
        if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
            throw new Exception("برای فعالسازی باشگاه مشتریان ابتدا باید پکیج پایه را خریداری کنید");

        if (user.ClubMembership?.IsActive == true)
            throw new Exception("باشگاه مشتریان شما قبلاً فعال شده است");

        // 3️⃣ تایید OTP
        var isOtpValid = await VerifyOtpAsync(
            user.MobileNumber, 
            "signClubContract", 
            request.OtpCode, 
            cancellationToken);

        if (!isOtpValid)
            throw new Exception("کد وارد شده اشتباه است یا منقضی شده است");

        // 4️⃣ ایجاد/دریافت Contract
        var contract = await _context.Contracts
            .FirstOrDefaultAsync(c => c.Type == ContractType.ClubMembership, cancellationToken);

        if (contract == null)
        {
            // اگر Contract وجود نداشته باشد، ایجاد می‌کنیم
            contract = new Contract
            {
                Type = ContractType.ClubMembership,
                Title = "قرارداد باشگاه مشتریان کارابازار",
                Description = "شرایط و ضوابط عضویت در باشگاه مشتریان",
                IsActive = true,
                CreatedAt = DateTime.Now
            };
            _context.Contracts.Add(contract);
            await _context.SaveChangesAsync(cancellationToken);
        }

        // 5️⃣ ثبت UserContract (امضای کاربر)
        var userContract = new UserContract
        {
            UserId = user.Id,
            ContractId = contract.Id,
            SignGuid = request.SignGuid,
            SignedPdfFile = request.ContractHtml, // HTML content ذخیره می‌شود
            IsAccepted = true,
            SignedAt = DateTime.Now
        };
        _context.UserContracts.Add(userContract);

        // 6️⃣ فعالسازی ClubMembership
        if (user.ClubMembership == null)
        {
            user.ClubMembership = new ClubMembership
            {
                UserId = user.Id,
                IsActive = true,
                ActivatedAt = DateTime.Now,
                InitialContribution = 56_000_000,  // مبلغ پکیج پایه
                GiftValue = 25_200_000,            // 45% هدیه
                PurchaseMethod = user.PackagePurchaseMethod
            };
            _context.ClubMemberships.Add(user.ClubMembership);
        }
        else
        {
            user.ClubMembership.IsActive = true;
            user.ClubMembership.ActivatedAt = DateTime.Now;
            user.ClubMembership.InitialContribution = 56_000_000;
            user.ClubMembership.GiftValue = 25_200_000;
            user.ClubMembership.PurchaseMethod = user.PackagePurchaseMethod;
        }

        await _context.SaveChangesAsync(cancellationToken);
        return true;
    }

    /// <summary>
    /// تایید کد OTP
    /// </summary>
    private async Task<bool> VerifyOtpAsync(
        string mobile, 
        string purpose, 
        string code, 
        CancellationToken cancellationToken)
    {
        var otpToken = await _context.OtpTokens
            .Where(o => o.Mobile == mobile 
                     && o.Purpose == purpose 
                     && o.Code == code 
                     && !o.IsUsed)
            .OrderByDescending(o => o.CreatedAt)
            .FirstOrDefaultAsync(cancellationToken);

        if (otpToken == null)
            return false;

        // بررسی انقضا (120 ثانیه)
        if ((DateTime.Now - otpToken.CreatedAt).TotalSeconds > 120)
            return false;

        // علامت‌گذاری به عنوان استفاده شده
        otpToken.IsUsed = true;
        await _context.SaveChangesAsync(cancellationToken);

        return true;
    }
}
```

**نکات کلیدی:**
- ✅ بررسی `PackagePurchaseMethod != None` (باید پکیج خریداری شده باشد)
- ✅ جلوگیری از امضای مجدد (`ClubMembership.IsActive == true`)
- ✅ تایید OTP با `VerifyOtpAsync` method
- ✅ ایجاد Contract اگر وجود نداشته باشد
- ✅ ثبت UserContract با SignGuid و HTML content
- ✅ فعالسازی ClubMembership با مقادیر مشخص شده

---

### 5️⃣ clubmembership.proto

```protobuf
syntax = "proto3";

option csharp_namespace = "CMSMicroservice.Protobuf";

package clubmembership;

service ClubMembershipContract {
  // ... other RPCs ...
  
  rpc AcceptClubMembershipContract(AcceptClubMembershipContractRequest) 
      returns (AcceptClubMembershipContractResponse);
}

message AcceptClubMembershipContractRequest {
  int64 user_id = 1;
  string otp_code = 2;
  string sign_guid = 3;
  string contract_html = 4;
}

message AcceptClubMembershipContractResponse {
  bool success = 1;
  string message = 2;
}
```

---

### 6️⃣ ClubMembershipService.cs

```csharp
public override async Task<AcceptClubMembershipContractResponse> AcceptClubMembershipContract(
    AcceptClubMembershipContractRequest request,
    ServerCallContext context)
{
    try
    {
        var command = _mapper.Map<AcceptClubMembershipContractCommand>(request);
        var result = await _mediator.Send(command);

        return new AcceptClubMembershipContractResponse
        {
            Success = result,
            Message = result ? "قرارداد با موفقیت امضا شد" : "خطا در امضای قرارداد"
        };
    }
    catch (Exception ex)
    {
        return new AcceptClubMembershipContractResponse
        {
            Success = false,
            Message = ex.Message
        };
    }
}
```

---

### 7️⃣ ClubFeatureProfile.cs

```csharp
using CMSMicroservice.Application.ClubMemberships.Commands.AcceptClubMembershipContract;
using CMSMicroservice.Protobuf;
using Mapster;

namespace CMSMicroservice.Application.Profiles;

public class ClubFeatureProfile : IRegister
{
    public void Register(TypeAdapterConfig config)
    {
        // ... other mappings ...

        config.NewConfig<AcceptClubMembershipContractRequest, AcceptClubMembershipContractCommand>()
            .Map(dest => dest.UserId, src => src.UserId)
            .Map(dest => dest.OtpCode, src => src.OtpCode)
            .Map(dest => dest.SignGuid, src => src.SignGuid)
            .Map(dest => dest.ContractHtml, src => src.ContractHtml);
    }
}
```

---

## 🔌 BFF Layer

### 📂 File Structure:
```
FrontOffice.BFF/
  src/FrontOffice.BFF.Domain/
    (No changes - using CMS entities)
  
  src/FrontOffice.BFF.Application/
    ClubMemberships/
      Commands/
        RequestClubContractOtp/
          RequestClubContractOtpCommand.cs           ✅ Created
          RequestClubContractOtpCommandValidator.cs  ✅ Created
          RequestClubContractOtpCommandHandler.cs    ✅ Created (با IKavenegarService)
        
        AcceptClubMembershipContract/
          AcceptClubMembershipContractCommand.cs           ✅ Created
          AcceptClubMembershipContractCommandValidator.cs  ✅ Created
          AcceptClubMembershipContractCommandHandler.cs    ✅ Created
    
    Profiles/
      ClubMembershipProfile.cs        ✅ Modified
  
  src/Protobufs/
    clubmembership.proto              ✅ Modified
  
  src/FrontOffice.BFF.WebApi/
    Services/
      ClubMembershipGrpcService.cs    ✅ Modified
```

---

### 1️⃣ RequestClubContractOtpCommand.cs

```csharp
using MediatR;

namespace FrontOffice.BFF.Application.ClubMemberships.Commands.RequestClubContractOtp;

/// <summary>
/// Command برای درخواست OTP برای امضای قرارداد باشگاه مشتریان
/// </summary>
public record RequestClubContractOtpCommand : IRequest<bool>
{
    /// <summary>
    /// شناسه یکتای امضاء (GUID) - برای ارسال در پیامک
    /// </summary>
    public required string SignGuid { get; init; }
}
```

---

### 2️⃣ RequestClubContractOtpCommandValidator.cs

```csharp
using FluentValidation;

namespace FrontOffice.BFF.Application.ClubMemberships.Commands.RequestClubContractOtp;

public class RequestClubContractOtpCommandValidator 
    : AbstractValidator<RequestClubContractOtpCommand>
{
    public RequestClubContractOtpCommandValidator()
    {
        RuleFor(x => x.SignGuid)
            .NotEmpty()
            .WithMessage("شناسه امضاء الزامی است")
            .Must(guid => Guid.TryParse(guid, out _))
            .WithMessage("شناسه امضاء نامعتبر است");
    }
}
```

---

### 3️⃣ RequestClubContractOtpCommandHandler.cs

```csharp
using System.Text;
using FrontOffice.BFF.Application.Common.Interfaces;
using MediatR;
using OtpService.Protobuf;

namespace FrontOffice.BFF.Application.ClubMemberships.Commands.RequestClubContractOtp;

public class RequestClubContractOtpCommandHandler 
    : IRequestHandler<RequestClubContractOtpCommand, bool>
{
    private readonly IApplicationContractContext _context;
    private readonly IKavenegarService _kavenegarService;
    private readonly ICurrentUserService _currentUserService;

    public RequestClubContractOtpCommandHandler(
        IApplicationContractContext context,
        IKavenegarService kavenegarService,
        ICurrentUserService currentUserService)
    {
        _context = context;
        _kavenegarService = kavenegarService;
        _currentUserService = currentUserService;
    }

    public async Task<bool> Handle(
        RequestClubContractOtpCommand request, 
        CancellationToken cancellationToken)
    {
        // 1️⃣ دریافت شماره موبایل از CurrentUserService
        var mobileNumber = _currentUserService.MobileNumber;
        
        if (string.IsNullOrEmpty(mobileNumber))
            throw new Exception("شماره موبایل کاربر یافت نشد");

        // 2️⃣ فراخوانی CMS برای ایجاد OTP
        var otpResponse = await _context.OtpToken.CreateNewOtpTokenAsync(
            new CreateNewOtpTokenRequest
            {
                Mobile = mobileNumber,
                Purpose = "signClubContract"
            },
            cancellationToken: cancellationToken);

        if (!otpResponse.Success || string.IsNullOrWhiteSpace(otpResponse.Code))
            throw new Exception("خطا در ارسال کد تایید");

        // 3️⃣ ارسال پیامک با Kavenegar
        var fullName = $"{_currentUserService.FirstName} {_currentUserService.LastName}".Trim();
        
        await _kavenegarService.Send(
            mobile: mobileNumber,
            new StringBuilder("سلام ")
                .Append(fullName)
                .AppendLine(" عزیز")
                .Append("کد یک بار مصرف برای تایید قرارداد باشگاه مشتریان: ")
                .AppendLine(otpResponse.Code)
                .AppendLine("شناسه امضاء: ")
                .AppendLine(request.SignGuid)
                .AppendLine("کارابازار")
                .ToString());

        return true;
    }
}
```

**نکات کلیدی:**
- ✅ استفاده از `IKavenegarService` برای ارسال پیامک (مشابه `CreateNewOtpTokenCommandHandler`)
- ✅ دریافت `MobileNumber` از `ICurrentUserService` (از JWT Token)
- ✅ Purpose: `signClubContract`
- ✅ ارسال `SignGuid` در پیامک برای ردیابی
- ✅ Format پیامک شامل: نام کاربر، کد OTP، شناسه امضا، نام شرکت

**SMS Format Example:**
```
سلام علی عزیز
کد یک بار مصرف برای تایید قرارداد باشگاه مشتریان: 123456
شناسه امضاء: a1b2c3d4-e5f6-7890-abcd-ef1234567890
کارابازار
```

---

### 4️⃣ AcceptClubMembershipContractCommand.cs

```csharp
using MediatR;

namespace FrontOffice.BFF.Application.ClubMemberships.Commands.AcceptClubMembershipContract;

/// <summary>
/// Command برای پذیرش و امضای قرارداد باشگاه مشتریان
/// </summary>
public record AcceptClubMembershipContractCommand : IRequest<string?>
{
    /// <summary>
    /// کد OTP ارسال شده به کاربر (6 رقمی)
    /// </summary>
    public required string OtpCode { get; init; }

    /// <summary>
    /// شناسه یکتای امضاء (GUID)
    /// </summary>
    public required string SignGuid { get; init; }

    /// <summary>
    /// محتوای HTML قرارداد برای ذخیره
    /// </summary>
    public required string ContractHtml { get; init; }
}
```

**Return Type:** `string?` - JWT Token جدید (اگر موفق بود)

---

### 5️⃣ AcceptClubMembershipContractCommandValidator.cs

```csharp
using FluentValidation;

namespace FrontOffice.BFF.Application.ClubMemberships.Commands.AcceptClubMembershipContract;

public class AcceptClubMembershipContractCommandValidator 
    : AbstractValidator<AcceptClubMembershipContractCommand>
{
    public AcceptClubMembershipContractCommandValidator()
    {
        RuleFor(x => x.OtpCode)
            .NotEmpty()
            .WithMessage("کد تایید الزامی است")
            .Length(6)
            .WithMessage("کد تایید باید 6 رقمی باشد")
            .Matches(@"^\d{6}$")
            .WithMessage("کد تایید فقط باید شامل اعداد باشد");

        RuleFor(x => x.SignGuid)
            .NotEmpty()
            .WithMessage("شناسه امضاء الزامی است")
            .Must(guid => Guid.TryParse(guid, out _))
            .WithMessage("شناسه امضاء نامعتبر است");

        RuleFor(x => x.ContractHtml)
            .NotEmpty()
            .WithMessage("محتوای قرارداد الزامی است")
            .MinimumLength(100)
            .WithMessage("محتوای قرارداد نامعتبر است");
    }
}
```

---

### 6️⃣ AcceptClubMembershipContractCommandHandler.cs

```csharp
using CMSMicroservice.Protobuf;
using FrontOffice.BFF.Application.Common.Interfaces;
using MediatR;
using User.Protobuf;

namespace FrontOffice.BFF.Application.ClubMemberships.Commands.AcceptClubMembershipContract;

public class AcceptClubMembershipContractCommandHandler 
    : IRequestHandler<AcceptClubMembershipContractCommand, string?>
{
    private readonly IApplicationContractContext _context;
    private readonly ICurrentUserService _currentUserService;

    public AcceptClubMembershipContractCommandHandler(
        IApplicationContractContext context,
        ICurrentUserService currentUserService)
    {
        _context = context;
        _currentUserService = currentUserService;
    }

    public async Task<string?> Handle(
        AcceptClubMembershipContractCommand request, 
        CancellationToken cancellationToken)
    {
        var userId = _currentUserService.UserId 
            ?? throw new Exception("کاربر احراز هویت نشده است");

        // 1️⃣ فراخوانی CMS برای امضای قرارداد
        var cmsResponse = await _context.ClubMemberships.AcceptClubMembershipContractAsync(
            new AcceptClubMembershipContractRequest
            {
                UserId = userId,
                OtpCode = request.OtpCode,
                SignGuid = request.SignGuid,
                ContractHtml = request.ContractHtml
            },
            cancellationToken: cancellationToken);

        if (!cmsResponse.Success)
            throw new Exception(cmsResponse.Message ?? "خطا در امضای قرارداد");

        // 2️⃣ دریافت JWT Token جدید
        var tokenResponse = await _context.User.GetJwtTokenAsync(
            new GetJwtTokenRequest { Id = userId },
            cancellationToken: cancellationToken);

        return tokenResponse?.Token;
    }
}
```

**نکات کلیدی:**
- ✅ دریافت `UserId` از `ICurrentUserService`
- ✅ فراخوانی CMS.AcceptClubMembershipContract
- ✅ **Automatic Token Refresh** بعد از موفقیت
- ✅ Return کردن token جدید به Frontend

---

### 7️⃣ clubmembership.proto (BFF)

```protobuf
syntax = "proto3";

option csharp_namespace = "FrontOffice.BFF.ClubMembership.Protobuf";

package clubmembership;

service ClubMembership {
  // ... other RPCs ...
  
  rpc RequestClubContractOtp(RequestClubContractOtpRequest) 
      returns (RequestClubContractOtpResponse);
  
  rpc AcceptClubMembershipContract(AcceptClubMembershipContractRequest) 
      returns (AcceptClubMembershipContractResponse);
}

message RequestClubContractOtpRequest {
  string sign_guid = 1;
}

message RequestClubContractOtpResponse {
  bool success = 1;
  string message = 2;
}

message AcceptClubMembershipContractRequest {
  string otp_code = 1;
  string sign_guid = 2;
  string contract_html = 3;
}

message AcceptClubMembershipContractResponse {
  bool success = 1;
  string message = 2;
  string new_token = 3;  // JWT Token جدید
}
```

---

### 8️⃣ ClubMembershipGrpcService.cs

```csharp
public override async Task<RequestClubContractOtpResponse> RequestClubContractOtp(
    RequestClubContractOtpRequest request,
    ServerCallContext context)
{
    try
    {
        var command = _mapper.Map<RequestClubContractOtpCommand>(request);
        var result = await _mediator.Send(command);

        return new RequestClubContractOtpResponse
        {
            Success = result,
            Message = result ? "کد تایید ارسال شد" : "خطا در ارسال کد تایید"
        };
    }
    catch (Exception ex)
    {
        return new RequestClubContractOtpResponse
        {
            Success = false,
            Message = ex.Message
        };
    }
}

public override async Task<AcceptClubMembershipContractResponse> AcceptClubMembershipContract(
    AcceptClubMembershipContractRequest request,
    ServerCallContext context)
{
    try
    {
        var command = _mapper.Map<AcceptClubMembershipContractCommand>(request);
        var newToken = await _mediator.Send(command);

        return new AcceptClubMembershipContractResponse
        {
            Success = true,
            Message = "قرارداد با موفقیت امضا شد",
            NewToken = newToken ?? string.Empty
        };
    }
    catch (Exception ex)
    {
        return new AcceptClubMembershipContractResponse
        {
            Success = false,
            Message = ex.Message,
            NewToken = string.Empty
        };
    }
}
```

---

### 9️⃣ ClubMembershipProfile.cs

```csharp
using FrontOffice.BFF.Application.ClubMemberships.Commands.AcceptClubMembershipContract;
using FrontOffice.BFF.Application.ClubMemberships.Commands.RequestClubContractOtp;
using FrontOffice.BFF.ClubMembership.Protobuf;
using Mapster;

namespace FrontOffice.BFF.Application.Profiles;

public class ClubMembershipProfile : IRegister
{
    public void Register(TypeAdapterConfig config)
    {
        // ... other mappings ...

        config.NewConfig<RequestClubContractOtpRequest, RequestClubContractOtpCommand>()
            .Map(dest => dest.SignGuid, src => src.SignGuid);

        config.NewConfig<AcceptClubMembershipContractRequest, AcceptClubMembershipContractCommand>()
            .Map(dest => dest.OtpCode, src => src.OtpCode)
            .Map(dest => dest.SignGuid, src => src.SignGuid)
            .Map(dest => dest.ContractHtml, src => src.ContractHtml);
    }
}
```

---

## 🎨 Frontend Layer

### 📂 File Structure:
```
FrontOffice/
  src/FrontOffice.Main/
    Pages/Profile/
      Index.razor.cs                         ✅ Modified
    
    Components/Dialogs/
      ClubMembershipContractDialog.razor     ✅ Created
```

---

### 1️⃣ ClubMembershipContractDialog.razor

```razor
@using FrontOffice.BFF.ClubMembership.Protobuf
@inject ClubMembership.ClubMembershipClient ClubMembershipClient
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
@inject ILocalStorageService LocalStorage
@implements IDisposable

<MudDialog>
    <DialogContent>
        <MudContainer MaxWidth="MaxWidth.Medium" Class="pa-4">
            @if (_currentStep == ContractStep.ReadContract)
            {
                <MudText Typo="Typo.h6" Class="mb-4">📜 قرارداد باشگاه مشتریان کارابازار</MudText>
                
                <MudPaper Class="pa-4 mb-4" Style="max-height: 400px; overflow-y: auto;">
                    @((MarkupString)GetClubContractHtml())
                </MudPaper>

                <MudAlert Severity="Severity.Info" Class="mb-4">
                    ⚠️ توجه: برای استفاده از امکانات باشگاه مشتریان و فعالسازی لینک دعوت، باید این قرارداد را امضا کنید.
                </MudAlert>

                <MudButton 
                    Variant="Variant.Filled" 
                    Color="Color.Primary" 
                    FullWidth="true"
                    OnClick="RequestOtp"
                    Disabled="_isLoading">
                    @if (_isLoading)
                    {
                        <MudProgressCircular Size="Size.Small" Indeterminate="true" />
                        <span class="ms-2">در حال ارسال...</span>
                    }
                    else
                    {
                        <span>✅ مطالعه کردم، درخواست کد تایید</span>
                    }
                </MudButton>
            }
            else if (_currentStep == ContractStep.EnterOtp)
            {
                <MudText Typo="Typo.h6" Class="mb-4">🔐 تایید امضای قرارداد</MudText>
                
                <MudAlert Severity="Severity.Success" Class="mb-4">
                    ✅ کد تایید به شماره موبایل شما ارسال شد.
                </MudAlert>

                <MudTextField 
                    @bind-Value="_otpCode"
                    Label="کد تایید (6 رقمی)"
                    Variant="Variant.Outlined"
                    MaxLength="6"
                    InputMode="InputMode.numeric"
                    HelperText="@($"زمان باقی‌مانده: {_remainingSeconds} ثانیه")"
                    Class="mb-4" />

                <MudButton 
                    Variant="Variant.Filled" 
                    Color="Color.Primary" 
                    FullWidth="true"
                    OnClick="AcceptContract"
                    Disabled="_isLoading || string.IsNullOrWhiteSpace(_otpCode) || _otpCode.Length != 6">
                    @if (_isLoading)
                    {
                        <MudProgressCircular Size="Size.Small" Indeterminate="true" />
                        <span class="ms-2">در حال تایید...</span>
                    }
                    else
                    {
                        <span>✍️ امضای قرارداد</span>
                    }
                </MudButton>

                <MudButton 
                    Variant="Variant.Text" 
                    Color="Color.Secondary" 
                    FullWidth="true"
                    Class="mt-2"
                    OnClick="RequestOtp"
                    Disabled="_isLoading || _remainingSeconds > 0">
                    🔄 ارسال مجدد کد
                </MudButton>
            }
            else if (_currentStep == ContractStep.Success)
            {
                <MudText Typo="Typo.h6" Class="mb-4 text-center">🎉 تبریک!</MudText>
                
                <MudAlert Severity="Severity.Success" Class="mb-4">
                    ✅ قرارداد با موفقیت امضا شد و باشگاه مشتریان شما فعال شد.
                    اکنون می‌توانید از لینک دعوت استفاده کنید.
                </MudAlert>

                <MudButton 
                    Variant="Variant.Filled" 
                    Color="Color.Primary" 
                    FullWidth="true"
                    OnClick="CloseAndRefresh">
                    ✅ متوجه شدم
                </MudButton>
            }
        </MudContainer>
    </DialogContent>
</MudDialog>

@code {
    [CascadingParameter] 
    private IMudDialogInstance MudDialog { get; set; } = null!;

    private enum ContractStep
    {
        ReadContract,
        EnterOtp,
        Success
    }

    private ContractStep _currentStep = ContractStep.ReadContract;
    private bool _isLoading;
    private string _signGuid = Guid.NewGuid().ToString();
    private string _otpCode = string.Empty;
    private int _remainingSeconds = 120;
    private System.Threading.Timer? _timer;

    private async Task RequestOtp()
    {
        _isLoading = true;
        try
        {
            var response = await ClubMembershipClient.RequestClubContractOtpAsync(
                new RequestClubContractOtpRequest { SignGuid = _signGuid });

            if (response.Success)
            {
                _currentStep = ContractStep.EnterOtp;
                _remainingSeconds = 120;
                StartTimer();
                Snackbar.Add("کد تایید ارسال شد", Severity.Success);
            }
            else
            {
                Snackbar.Add(response.Message ?? "خطا در ارسال کد تایید", Severity.Error);
            }
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
        }
        finally
        {
            _isLoading = false;
        }
    }

    private async Task AcceptContract()
    {
        _isLoading = true;
        try
        {
            var response = await ClubMembershipClient.AcceptClubMembershipContractAsync(
                new AcceptClubMembershipContractRequest
                {
                    OtpCode = _otpCode,
                    SignGuid = _signGuid,
                    ContractHtml = GetClubContractHtml()
                });

            if (response.Success)
            {
                // ذخیره token جدید
                if (!string.IsNullOrEmpty(response.NewToken))
                {
                    await LocalStorage.SetItemAsStringAsync("token", response.NewToken);
                }

                _currentStep = ContractStep.Success;
                StopTimer();
                Snackbar.Add("قرارداد با موفقیت امضا شد", Severity.Success);
            }
            else
            {
                Snackbar.Add(response.Message ?? "خطا در امضای قرارداد", Severity.Error);
            }
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
        }
        finally
        {
            _isLoading = false;
        }
    }

    private void CloseAndRefresh()
    {
        // بستن modal و refresh صفحه برای نمایش لینک دعوت
        Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
    }

    private void StartTimer()
    {
        _timer = new System.Threading.Timer(_ =>
        {
            if (_remainingSeconds > 0)
            {
                _remainingSeconds--;
                InvokeAsync(StateHasChanged);
            }
            else
            {
                StopTimer();
            }
        }, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
    }

    private void StopTimer()
    {
        _timer?.Dispose();
        _timer = null;
    }

    public void Dispose()
    {
        StopTimer();
    }

    private string GetClubContractHtml()
    {
        return @"
            <div style='font-family: IRANSans, Tahoma; line-height: 1.8;'>
                <h3 style='text-align: center;'>قرارداد عضویت در باشگاه مشتریان کارابازار</h3>
                
                <p>این قرارداد بین کاربر محترم (عضو باشگاه) و شرکت کارابازار منعقد می‌گردد.</p>
                
                <h4>ماده 1: تعهدات شرکت</h4>
                <ul>
                    <li>ارائه خدمات باشگاه مشتریان طبق شرایط اعلام شده</li>
                    <li>امکان دعوت سایر کاربران از طریق لینک دعوت اختصاصی</li>
                    <li>دریافت کمیسیون از خریدهای زیرمجموعه‌ها</li>
                </ul>

                <h4>ماده 2: تعهدات کاربر</h4>
                <ul>
                    <li>رعایت قوانین و مقررات باشگاه مشتریان</li>
                    <li>عدم سوء استفاده از لینک دعوت</li>
                    <li>رعایت اصول اخلاقی در معرفی افراد</li>
                </ul>

                <h4>ماده 3: جزئیات مالی</h4>
                <ul>
                    <li>مبلغ پرداختی: 56,000,000 تومان</li>
                    <li>ارزش هدیه: 25,200,000 تومان (45% مبلغ پرداختی)</li>
                    <li>کل شارژ کیف پول: 56,000,000 تومان</li>
                </ul>

                <p style='margin-top: 20px;'>
                    <strong>با امضای این قرارداد، شما تمامی شرایط و ضوابط فوق را می‌پذیرید.</strong>
                </p>
            </div>
        ";
    }
}
```

**نکات کلیدی:**
- ✅ استفاده از `IMudDialogInstance` (نه `MudDialogInstance`)
- ✅ سه مرحله: ReadContract → EnterOtp → Success
- ✅ Timer countdown برای OTP (120 ثانیه)
- ✅ ذخیره token جدید در localStorage
- ✅ Refresh صفحه بعد از موفقیت
- ✅ HTML contract content در `GetClubContractHtml()`

---

### 2️⃣ Index.razor.cs (Profile Page)

```csharp
private bool _hasPurchasedPackage;
private bool _isClubMemberActive;

private bool CanShowReferralLink => _hasPurchasedPackage && _isClubMemberActive;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        await LoadUserData();
        await CheckAndShowClubContractModal();
        StateHasChanged();
    }
}

private async Task CheckAndShowClubContractModal()
{
    // اگر کاربر پکیج خریده ولی قرارداد امضا نکرده
    if (_hasPurchasedPackage && !_isClubMemberActive)
    {
        var options = new DialogOptions
        {
            BackdropClick = false,      // غیرقابل بسته شدن با کلیک بیرون
            CloseOnEscapeKey = false,   // غیرقابل بسته شدن با Escape
            CloseButton = false,        // بدون دکمه Close
            MaxWidth = MaxWidth.Medium,
            FullWidth = true
        };

        await DialogService.ShowAsync<ClubMembershipContractDialog>("", options);
    }
}
```

**نکات کلیدی:**
- ✅ `BackdropClick = false` (نه `DisableBackdropClick`)
- ✅ `CloseOnEscapeKey = false`
- ✅ `CloseButton = false`
- ✅ فراخوانی در `OnAfterRenderAsync`

---

## 📱 OTP SMS Format

### Message Template:
```
سلام {نام کاربر} عزیز
کد یک بار مصرف برای تایید قرارداد باشگاه مشتریان: {کد 6 رقمی}
شناسه امضاء: {GUID}
کارابازار
```

### Real Example:
```
سلام علی احمدی عزیز
کد یک بار مصرف برای تایید قرارداد باشگاه مشتریان: 123456
شناسه امضاء: a1b2c3d4-e5f6-7890-abcd-ef1234567890
کارابازار
```

### Code Implementation:
```csharp
await _kavenegarService.Send(
    mobile: mobileNumber,
    new StringBuilder("سلام ")
        .Append(fullName)
        .AppendLine(" عزیز")
        .Append("کد یک بار مصرف برای تایید قرارداد باشگاه مشتریان: ")
        .AppendLine(otpResponse.Code)
        .AppendLine("شناسه امضاء: ")
        .AppendLine(request.SignGuid)
        .AppendLine("کارابازار")
        .ToString());
```

---

## 🔄 Token Refresh Pattern

### چرا Token Refresh؟
بعد از امضای قرارداد، وضعیت کاربر تغییر می‌کند:
- `ClubMembership.IsActive` از `false` به `true` تغییر می‌کند
- JWT Token فعلی claim‌های قدیمی دارد
- برای نمایش لینک دعوت، نیاز به token جدید با claim‌های به‌روز شده داریم

### Flow:
```
1. AcceptContract موفق شد
   ↓
2. BFF.AcceptClubMembershipContractCommandHandler
   ├─ فراخوانی CMS.AcceptClubMembershipContract
   └─ فراخوانی CMS.GetJwtToken(userId) → token جدید
   ↓
3. Frontend دریافت token جدید
   └─ ذخیره در localStorage
   ↓
4. Refresh صفحه
   └─ لینک دعوت نمایش داده می‌شود
```

### Code:
```csharp
// BFF Handler
var tokenResponse = await _context.User.GetJwtTokenAsync(
    new GetJwtTokenRequest { Id = userId },
    cancellationToken: cancellationToken);

return tokenResponse?.Token;
```

```csharp
// Frontend
if (!string.IsNullOrEmpty(response.NewToken))
{
    await LocalStorage.SetItemAsStringAsync("token", response.NewToken);
}

Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
```

---

## ✅ Testing Checklist

### 1️⃣ CMS Layer Tests:
- [ ] `AcceptClubMembershipContractCommandValidator` validation rules
- [ ] `AcceptClubMembershipContractCommandHandler`:
  - [ ] کاربر یافت نمی‌شود → Exception
  - [ ] PackagePurchaseMethod = None → Exception
  - [ ] ClubMembership.IsActive = true → Exception (جلوگیری از امضای مجدد)
  - [ ] OTP نامعتبر → Exception
  - [ ] OTP منقضی شده → Exception
  - [ ] امضای موفق → ClubMembership.IsActive = true
  - [ ] مقادیر صحیح: InitialContribution, GiftValue, ActivatedAt

### 2️⃣ BFF Layer Tests:
- [ ] `RequestClubContractOtpCommandHandler`:
  - [ ] MobileNumber از CurrentUserService دریافت می‌شود
  - [ ] OTP از CMS دریافت می‌شود
  - [ ] پیامک با IKavenegarService ارسال می‌شود
  - [ ] SignGuid در پیامک موجود است
- [ ] `AcceptClubMembershipContractCommandHandler`:
  - [ ] فراخوانی CMS موفق
  - [ ] Token جدید دریافت و return می‌شود

### 3️⃣ Frontend Tests:
- [ ] Modal نمایش داده می‌شود وقتی `HasPurchasedPackage && !IsClubMemberActive`
- [ ] Modal **غیرقابل بسته شدن** است (Escape, Backdrop Click, Close Button)
- [ ] درخواست OTP موفق → مرحله EnterOtp
- [ ] Timer countdown کار می‌کند (120 ثانیه)
- [ ] امضای موفق → مرحله Success
- [ ] Token refresh و reload صفحه
- [ ] لینک دعوت نمایش داده می‌شود

### 4️⃣ Integration Tests:
- [ ] End-to-End Flow: Payment → Modal → OTP → Sign → Refresh → Referral Link
- [ ] پیامک واقعی ارسال می‌شود
- [ ] Contract و UserContract در دیتابیس ثبت می‌شود
- [ ] ClubMembership فعال می‌شود

---

## 🔗 Related Documents

- [Base Package Payment System](./base-package-payment-system.md)
- [Network Commission System](./network-commission-system.md)
- [Binary Tree Guide](./binary-tree-guide.md)

---

## 📝 Notes

### تغییرات مهم:
1. **ContractType.ClubMembership** - نام تغییر کرد از `CMS` به `ClubMembership`
2. **IKavenegarService** - الزامی برای ارسال پیامک در BFF
3. **IMudDialogInstance** - type صحیح برای MudDialog
4. **BackdropClick** - جایگزین `DisableBackdropClick`

### نکات امنیتی:
- ✅ OTP Verification قبل از امضا
- ✅ جلوگیری از امضای مجدد (ClubMembership.IsActive check)
- ✅ بررسی PackagePurchaseMethod (کاربر باید پکیج خریده باشد)
- ✅ Token Refresh برای claims جدید

### Known Issues:
- هیچ موردی گزارش نشده ✅

---

**آخرین به‌روزرسانی:** 2024-12-16  
**مستندساز:** GitHub Copilot  
**وضعیت Build:** ✅ All Green (CMS, BFF, Frontend)
