Files
docs/01-BUSINESS/manual-payment-system.md
T
masoodafar-web 201915d8c5 update
2025-12-05 17:28:24 +03:30

18 KiB

Manual Payment System (سیستم پرداخت دستی مشتریان)

📌 Overview

سیستم پرداخت دستی برای مشتریانی که بدون خرید وام دایا می‌خواهند مستقیماً 56 میلیون تومان پرداخت کنند و همان مزایا را دریافت کنند.

🎯 سناریوها

سناریو 1: پرداخت آنلاین (درگاه پرداخت)

کاربر → انتخاب گزینه "پرداخت دستی" در فرانت‌آفیس
  ↓
ایجاد Transaction با Type=ManualPaymentOnline, Amount=56M, Status=Pending
  ↓
ریدایرکت به درگاه پرداخت (Zarinpal/Mellat/...)
  ↓
Callback از درگاه با RefId
  ↓
VerifyManualPaymentCommand → تایید تراکنش
  ↓
شارژ کیف‌پول‌ها (Balance=56M, NetworkBalance=56M, DiscountBalance=56M)
  ↓
فعال‌سازی عضویت باشگاه (ClubMembership)

سناریو 2: کارت‌به‌کارت با تایید ادمین

کاربر → کارت‌به‌کارت 56 میلیون + ارسال تصویر رسید
  ↓
CreateManualPaymentRequestCommand → ثبت درخواست با Status=PendingAdminApproval
  - تصویر رسید + کد پیگیری استخراج شده توسط کاربر
  ↓
ادمین → بررسی درخواست در BackOffice
  ↓
ApproveManualPaymentCommand یا RejectManualPaymentCommand
  ↓
در صورت تایید: 
  - ایجاد Transaction با RefId=کد پیگیری
  - شارژ کیف‌پول‌ها
  - فعال‌سازی عضویت باشگاه
  ↓
در صورت رد:
  - ثبت دلیل رد
  - اطلاع‌رسانی به کاربر

🗂️ Architecture

Domain Layer

وضعیت فعلی پیاده‌سازی (CMS)
در نسخه‌ای که الآن در CMS داریم، سناریوی «درخواست پرداخت دستی توسط کاربر» (ManualPaymentRequest + Verify از درگاه) هنوز پیاده‌سازی نشده و فقط بخش پرداخت دستی توسط Admin/SuperAdmin با Entity ساده‌تر ManualPayment و Enumهای ManualPaymentType و ManualPaymentStatus (Pending/Approved/Rejected/Cancelled) اجرا شده است.
بخش‌های زیر که با ManualPaymentRequest، ManualPaymentMethod و Verify/ProcessManualPayment توضیح داده شده‌اند، طراحی کامل سیستم هستند و برای فاز بعدی (FrontOffice + OnlineGateway/CardToCard) استفاده خواهند شد.

ManualPaymentStatus Enum (طراحی کامل – برای Requestها)

public enum ManualPaymentStatus
{
    PendingAdminApproval = 0,  // در انتظار تایید ادمین (کارت‌به‌کارت)
    PendingPayment = 1,         // در انتظار پرداخت (درگاه آنلاین)
    PaymentVerified = 2,        // پرداخت تایید شده (از درگاه)
    AdminApproved = 3,          // تایید شده توسط ادمین
    AdminRejected = 4,          // رد شده توسط ادمین
    Completed = 5,              // تکمیل شده (کیف‌پول شارژ شده)
    Failed = 6                  // خطا در پردازش
}

ManualPaymentMethod Enum

public enum ManualPaymentMethod
{
    OnlineGateway = 0,     // درگاه آنلاین
    CardToCard = 1         // کارت‌به‌کارت
}

ManualPaymentRequest Entity (طراحی کامل – هنوز پیاده نشده)

public class ManualPaymentRequest : BaseAuditableEntity
{
    public long UserId { get; set; }
    public ManualPaymentMethod Method { get; set; }
    public ManualPaymentStatus Status { get; set; }
    public long Amount { get; set; } = 56_000_000; // مبلغ ثابت
    
    // آنلاین Gateway
    public string? GatewayName { get; set; }        // Zarinpal, Mellat, etc.
    public string? GatewayTrackingCode { get; set; } // کد پیگیری درگاه
    public DateTime? GatewayPaymentDate { get; set; }
    
    // کارت‌به‌کارت
    public string? ReceiptImageUrl { get; set; }     // مسیر تصویر رسید
    public string? UserProvidedTrackingCode { get; set; } // کد پیگیری که کاربر داده
    public DateTime? CardToCardDate { get; set; }
    
    // تایید/رد ادمین
    public long? ApprovedByAdminId { get; set; }
    public DateTime? AdminDecisionDate { get; set; }
    public string? AdminNotes { get; set; }         // توضیحات ادمین (دلیل رد)
    
    // تراکنش نهایی
    public long? TransactionId { get; set; }
    public bool IsProcessed { get; set; }
    public DateTime? ProcessedDate { get; set; }
    
    // Navigation Properties
    public virtual User User { get; set; }
    public virtual User? ApprovedByAdmin { get; set; }
    public virtual Transactions? Transaction { get; set; }
}

Application Layer

Commands

1. CreateManualPaymentRequestCommand (FrontOffice)

ایجاد درخواست پرداخت دستی توسط کاربر

Request:

public record CreateManualPaymentRequestCommand : IRequest<CreateManualPaymentRequestResponseDto>
{
    public long UserId { get; init; }
    public ManualPaymentMethod Method { get; init; }
    
    // برای OnlineGateway
    public string? GatewayName { get; init; }
    public string? ReturnUrl { get; init; } // URL بازگشت بعد از پرداخت
    
    // برای CardToCard
    public IFormFile? ReceiptImage { get; init; }     // فایل تصویر رسید
    public string? TrackingCode { get; init; }         // کد پیگیری
    public DateTime? TransactionDate { get; init; }
}

Response:

public class CreateManualPaymentRequestResponseDto
{
    public long RequestId { get; set; }
    public ManualPaymentStatus Status { get; set; }
    
    // برای OnlineGateway: URL پرداخت
    public string? PaymentUrl { get; set; }
    
    // برای CardToCard: پیام موفقیت
    public string Message { get; set; }
}

Business Logic:

  1. بررسی اینکه کاربر قبلاً درخواست Pending ندارد
  2. اگر Method=OnlineGateway:
    • ایجاد ManualPaymentRequest با Status=PendingPayment
    • فراخوانی Gateway Service برای دریافت URL پرداخت
    • ذخیره GatewayName و کد درخواست
    • برگرداندن PaymentUrl به کاربر
  3. اگر Method=CardToCard:
    • آپلود تصویر رسید به Storage
    • ایجاد ManualPaymentRequest با Status=PendingAdminApproval
    • ذخیره UserProvidedTrackingCode و CardToCardDate
    • ارسال نوتیفیکیشن به ادمین‌ها
2. VerifyManualPaymentCommand (Callback از درگاه)

تایید پرداخت آنلاین بعد از بازگشت از درگاه

Request:

public record VerifyManualPaymentCommand : IRequest<VerifyManualPaymentResponseDto>
{
    public long RequestId { get; init; }
    public string GatewayTrackingCode { get; init; }
    public string? Authority { get; init; } // پارامتر درگاه
}

Business Logic:

  1. یافتن ManualPaymentRequest با Status=PendingPayment
  2. فراخوانی Gateway Service برای Verify کردن تراکنش
  3. اگر تایید شد:
    • به‌روزرسانی Status → PaymentVerified
    • ذخیره GatewayTrackingCode و GatewayPaymentDate
    • فراخوانی ProcessManualPaymentCommand برای شارژ کیف‌پول
  4. اگر رد شد:
    • به‌روزرسانی Status → Failed
3. ApproveManualPaymentCommand (Admin)

تایید درخواست کارت‌به‌کارت توسط ادمین

Request:

public record ApproveManualPaymentCommand : IRequest<ApproveManualPaymentResponseDto>
{
    public long RequestId { get; init; }
    public long AdminUserId { get; init; }
    public string? AdminNotes { get; init; }
}

Business Logic:

  1. بررسی RequestId موجود با Status=PendingAdminApproval
  2. بررسی دسترسی ادمین
  3. به‌روزرسانی:
    • Status → AdminApproved
    • ApprovedByAdminId, AdminDecisionDate, AdminNotes
  4. فراخوانی ProcessManualPaymentCommand برای شارژ کیف‌پول
4. RejectManualPaymentCommand (Admin)

رد درخواست کارت‌به‌کارت توسط ادمین

Request:

public record RejectManualPaymentCommand : IRequest<RejectManualPaymentResponseDto>
{
    public long RequestId { get; init; }
    public long AdminUserId { get; init; }
    public string RejectionReason { get; init; } // الزامی
}

Business Logic:

  1. بررسی RequestId موجود
  2. به‌روزرسانی:
    • Status → AdminRejected
    • ApprovedByAdminId, AdminDecisionDate
    • AdminNotes = RejectionReason
  3. ارسال نوتیفیکیشن به کاربر با دلیل رد
5. ProcessManualPaymentCommand (Internal)

شارژ کیف‌پول‌ها بعد از تایید پرداخت

این Command داخلی است و فقط توسط Verify یا Approve فراخوانی می‌شود.

Business Logic:

  1. ایجاد Transaction:
    • Type: DepositManual
    • Amount: 56M
    • RefId: GatewayTrackingCode یا UserProvidedTrackingCode
  2. شارژ Balance: +56M
  3. شارژ NetworkBalance: +56M
  4. شارژ DiscountBalance: +56M
  5. فعال‌سازی ClubMembership (اگر غیرفعال باشد)
  6. ثبت UserWalletChangeLog
  7. به‌روزرسانی ManualPaymentRequest:
    • Status → Completed
    • TransactionId, IsProcessed=true, ProcessedDate
  8. ارسال نوتیفیکیشن موفقیت به کاربر
6. GetUserManualPaymentHistoryQuery

دریافت تاریخچه پرداخت‌های دستی کاربر

Request:

public record GetUserManualPaymentHistoryQuery : IRequest<List<ManualPaymentHistoryDto>>
{
    public long UserId { get; init; }
}
7. GetPendingManualPaymentsQuery (Admin)

دریافت لیست درخواست‌های در انتظار تایید

Request:

public record GetPendingManualPaymentsQuery : IRequest<List<PendingManualPaymentDto>>
{
    public ManualPaymentStatus? StatusFilter { get; init; } = ManualPaymentStatus.PendingAdminApproval;
    public int PageNumber { get; init; } = 1;
    public int PageSize { get; init; } = 20;
}

💾 Database Schema

ManualPaymentRequests Table

CREATE TABLE [CMS].[ManualPaymentRequests] (
    [Id] bigint IDENTITY(1,1) PRIMARY KEY,
    [UserId] bigint NOT NULL FOREIGN KEY REFERENCES Users(Id),
    [Method] int NOT NULL,
    [Status] int NOT NULL,
    [Amount] bigint NOT NULL DEFAULT 56000000,
    
    -- آنلاین Gateway
    [GatewayName] nvarchar(50) NULL,
    [GatewayTrackingCode] nvarchar(200) NULL,
    [GatewayPaymentDate] datetime2 NULL,
    
    -- کارت‌به‌کارت
    [ReceiptImageUrl] nvarchar(500) NULL,
    [UserProvidedTrackingCode] nvarchar(200) NULL,
    [CardToCardDate] datetime2 NULL,
    
    -- تایید ادمین
    [ApprovedByAdminId] bigint NULL FOREIGN KEY REFERENCES Users(Id),
    [AdminDecisionDate] datetime2 NULL,
    [AdminNotes] nvarchar(max) NULL,
    
    -- تراکنش
    [TransactionId] bigint NULL FOREIGN KEY REFERENCES Transactionss(Id),
    [IsProcessed] bit NOT NULL DEFAULT 0,
    [ProcessedDate] datetime2 NULL,
    
    -- Audit
    [Created] datetime2 NOT NULL,
    [CreatedBy] nvarchar(max) NULL,
    [LastModified] datetime2 NULL,
    [LastModifiedBy] nvarchar(max) NULL,
    [IsDeleted] bit NOT NULL DEFAULT 0
);

CREATE INDEX IX_ManualPaymentRequests_UserId ON ManualPaymentRequests(UserId);
CREATE INDEX IX_ManualPaymentRequests_Status ON ManualPaymentRequests(Status);
CREATE INDEX IX_ManualPaymentRequests_TransactionId ON ManualPaymentRequests(TransactionId);

🔄 Process Flows

Flow 1: پرداخت آنلاین

sequenceDiagram
    participant User as کاربر
    participant FrontOffice as FrontOffice
    participant CMS as CMS API
    participant Gateway as درگاه پرداخت
    
    User->>FrontOffice: انتخاب "پرداخت دستی"
    FrontOffice->>CMS: CreateManualPaymentRequest (Method=OnlineGateway)
    CMS->>Gateway: ایجاد درخواست پرداخت
    Gateway-->>CMS: PaymentUrl
    CMS-->>FrontOffice: PaymentUrl
    FrontOffice->>Gateway: ریدایرکت کاربر
    User->>Gateway: پرداخت 56M
    Gateway->>CMS: Callback (RefId, Authority)
    CMS->>Gateway: Verify Payment
    Gateway-->>CMS: تایید پرداخت
    CMS->>CMS: ProcessManualPayment (شارژ کیف‌پول)
    CMS-->>FrontOffice: موفقیت
    FrontOffice-->>User: پرداخت موفق

Flow 2: کارت‌به‌کارت

sequenceDiagram
    participant User as کاربر
    participant FrontOffice as FrontOffice
    participant CMS as CMS API
    participant Admin as ادمین (BackOffice)
    
    User->>User: کارت‌به‌کارت 56M
    User->>FrontOffice: آپلود رسید + کد پیگیری
    FrontOffice->>CMS: CreateManualPaymentRequest (Method=CardToCard)
    CMS->>CMS: ذخیره تصویر + Status=PendingAdminApproval
    CMS-->>Admin: نوتیفیکیشن (درخواست جدید)
    Admin->>CMS: GetPendingManualPayments
    CMS-->>Admin: لیست درخواست‌ها
    Admin->>Admin: بررسی رسید و کد پیگیری
    
    alt تایید
        Admin->>CMS: ApproveManualPayment
        CMS->>CMS: ProcessManualPayment (شارژ کیف‌پول)
        CMS-->>User: نوتیفیکیشن موفقیت
    else رد
        Admin->>CMS: RejectManualPayment (دلیل رد)
        CMS-->>User: نوتیفیکیشن رد با دلیل
    end

🧪 Testing Scenarios

Test 1: پرداخت آنلاین موفق

# Step 1: ایجاد درخواست
POST /api/manualpayment/create
{
  "userId": 123,
  "method": 0,
  "gatewayName": "Zarinpal",
  "returnUrl": "https://example.com/callback"
}

# Response: PaymentUrl

# Step 2: کاربر پرداخت می‌کند (Mock Gateway)

# Step 3: Callback
POST /api/manualpayment/verify
{
  "requestId": 456,
  "gatewayTrackingCode": "ZP-12345",
  "authority": "A00000000..."
}

# Result: کیف‌پول شارژ شده، باشگاه فعال

Test 2: کارت‌به‌کارت با تایید ادمین

# Step 1: ایجاد درخواست کاربر
POST /api/manualpayment/create
{
  "userId": 123,
  "method": 1,
  "receiptImage": <file>,
  "trackingCode": "REF-98765",
  "transactionDate": "2024-12-01T10:00:00Z"
}

# Step 2: ادمین بررسی می‌کند
GET /api/admin/manualpayment/pending

# Step 3: ادمین تایید می‌کند
POST /api/admin/manualpayment/approve
{
  "requestId": 456,
  "adminUserId": 1,
  "adminNotes": "رسید معتبر است"
}

# Result: کیف‌پول شارژ شده

📋 Implementation Tasks

CMS Microservice

Domain Layer

  • ایجاد ManualPaymentStatus enum
  • ایجاد ManualPaymentMethod enum
  • ایجاد ManualPaymentRequest entity
  • اضافه کردن به ApplicationDbContext

Application Layer

  • CreateManualPaymentRequestCommand + Handler + Validator
  • VerifyManualPaymentCommand + Handler
  • ApproveManualPaymentCommand + Handler
  • RejectManualPaymentCommand + Handler
  • ProcessManualPaymentCommand + Handler (Internal)
  • GetUserManualPaymentHistoryQuery + Handler
  • GetPendingManualPaymentsQuery + Handler
  • Interface: IPaymentGatewayService
  • Interface: IFileStorageService (برای آپلود تصویر)

Infrastructure Layer

  • ZarinpalGatewayService : IPaymentGatewayService
  • LocalFileStorageService : IFileStorageService
  • Migration: AddManualPaymentSystem

WebApi Layer (Protobuf/gRPC)

  • Proto definitions: ManualPayment.proto
  • gRPC Service: ManualPaymentService

FrontOffice

Components

  • ManualPaymentPage.razor - صفحه انتخاب روش پرداخت
  • OnlinePaymentForm.razor - فرم پرداخت آنلاین
  • CardToCardForm.razor - فرم کارت‌به‌کارت (آپلود رسید)
  • PaymentCallbackPage.razor - صفحه بازگشت از درگاه
  • PaymentHistoryPage.razor - تاریخچه پرداخت‌های کاربر

Services

  • ManualPaymentService.cs - فراخوانی BFF

FrontOffice.BFF

Application Layer

  • CQRS Handlers برای مپ کردن gRPC به REST
  • DTOs برای API های REST

WebApi Layer

  • ManualPaymentController.cs - REST endpoints

BackOffice

Components

  • PendingPaymentsPage.razor - لیست درخواست‌های در انتظار
  • PaymentRequestDetailsModal.razor - جزئیات + نمایش رسید
  • ApproveRejectButtons.razor - دکمه‌های تایید/رد

Services

  • ManualPaymentAdminService.cs - فراخوانی BFF

BackOffice.BFF

Application Layer

  • Admin CQRS Handlers
  • Admin DTOs

WebApi Layer

  • AdminManualPaymentController.cs - REST endpoints برای ادمین

⚠️ Important Notes

1. Transaction Type

  • برای پرداخت دستی از TransactionType.DepositManual استفاده شود
  • RefId = GatewayTrackingCode (آنلاین) یا UserProvidedTrackingCode (کارت‌به‌کارت)

2. Security

  • تایید پرداخت درگاه باید با Signature Verification انجام شود
  • تصاویر رسید باید با Validation بارگذاری شوند (حجم، فرمت، محتوا)
  • فقط ادمین‌ها حق تایید/رد کارت‌به‌کارت دارند

3. Idempotency

  • نباید کاربر بتواند چند درخواست همزمان Pending داشته باشد
  • هر RequestId فقط یک بار قابل Verify است

4. Notifications

  • SMS/Email به کاربر بعد از:
    • ایجاد درخواست کارت‌به‌کارت
    • تایید/رد ادمین
    • موفقیت پرداخت آنلاین

5. File Storage

  • تصاویر رسید باید با GUID ذخیره شوند
  • مسیر: /uploads/receipts/{year}/{month}/{guid}.jpg
  • حداکثر حجم: 2MB
  • فرمت‌های مجاز: JPG, PNG, PDF


Created: 2024-12-01
Status: ⚠️ Not Implemented Yet (Design Complete)
Priority: High (برای کاربران بدون وام دایا ضروری است)