20 KiB
Base Package Payment System - سیستم پرداخت پکیج پایه
تاریخ ایجاد: 2024-12-16
تاریخ آخرین بهروزرسانی: 2024-12-16
وضعیت: ✅ پیادهسازی شده
اولویت: 🔴 بسیار بالا
📋 فهرست
- خلاصه سیستم
- Business Requirements
- معماری سیستم
- Implementation Details
- Club Membership Contract System
- API Endpoints
- Flow Diagram
- نکات مهم
🎯 خلاصه سیستم
سیستم پرداخت پکیج پایه امکان پرداخت 56 میلیون تومان را برای کاربران فراهم میکند تا بتوانند:
- کیف پول خود را شارژ کنند (Balance + DiscountBalance)
- امضای قرارداد باشگاه مشتریان (گام الزامی بعد از پرداخت)
- فعالسازی لینک دعوت (Referral Link) - تنها بعد از امضای قرارداد
- دسترسی کامل به امکانات باشگاه مشتریان
دو روش پرداخت:
- پرداخت مستقیم (Direct Payment) - از طریق درگاه بانکی (زرینپال)
- اعتبار الماسی دایا (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:
-
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
- بررسی عدم خرید قبلی:
-
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:
-
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), ... }; -
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:
-
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); } } -
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:
- UserId از JWT گرفته میشود نه از Request - امنیت بالاتر
- Validation در هر لایه انجام میشود
- Transaction Idempotency - چک میشود که Order Pending قبلی وجود نداشته باشد
Business Logic:
- کاربر فقط یک بار میتواند پکیج پایه بخرد
- شارژ همزمان Balance و DiscountBalance انجام میشود
- PackagePurchaseMethod بعد از پرداخت موفق به
DirectPurchaseتغییر میکند - برای فعالسازی لینک دعوت، باید هم پکیج خریداری شود هم باشگاه فعال شود
Error Handling:
- اگر CMS خطا برگرداند، به درگاه نمیرویم
- اگر PYMS URL ندهد، Transaction در CMS باقی میماند (Pending)
- اگر Callback با Status=NOK بیاید، مستقیماً Reject میشود
- اگر 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