docs: consolidate 53 files into 15 structured files in 3 folders

- business/ (5): club-commission, payment, ecommerce, membership, content
- technical/ (5): cms-arch, ui, deployment, migration, api
- overview/ (5): flowcharts, index, changelog, glossary, roadmap
- Removed all old folders: backoffice, cms, deployment, docs, frontoffice, migration, ui-modernization, business (old)
- Updated internal links with relative folder paths
This commit is contained in:
masoodafar-web
2026-02-18 22:29:37 +03:30
parent d7c32dab2a
commit efff5e9cd5
71 changed files with 3632 additions and 32267 deletions
-153
View File
@@ -1,153 +0,0 @@
# 🔀 جداسازی سرویس‌های Admin و Customer
> آخرین بروزرسانی: February 10, 2026
> مرتبط با: [ICURRENTUSERSERVICE-IMPLEMENTATION.md](ICURRENTUSERSERVICE-IMPLEMENTATION.md)
---
## 🐛 مشکل
پنل ادمین BackOffice بجای نمایش اطلاعات **همه کاربران**، فقط اطلاعات **خود ادمین** رو نشان میداد.
### علت ریشه‌ای:
Query Handler ها وقتی `UserId = 0` دریافت می‌کردند، بجای اینکه "همه کاربران" رو برگردانند، به JWT fallback می‌کردند و UserId ادمین رو از توکن استخراج می‌کردند:
```csharp
// ❌ الگوی قدیمی (مشکل‌دار)
var userId = request.UserId == 0
? (long.TryParse(_currentUser.UserId, out var uid) ? uid : 0) // ← fallback به JWT
: request.UserId;
```
### مشکل:
- **BackOffice (Admin)** → `UserId = 0` ارسال میکنه → Handler از JWT ادمین میخونه → فقط اطلاعات ادمین برمیگرده
- **FrontOffice (Customer)** → `UserId = 0` ارسال میکنه → Handler از JWT مشتری میخونه → اتفاقاً درسته، ولی دلیلش اشتباهه
---
## ✅ الگوی جدید
### اصل طراحی:
> **Handler ها بی‌خبر از JWT هستند.** وظیفه resolve کردن کاربر، به عهده **Service Layer (gRPC endpoint)** است.
### الگوی Handler:
```csharp
// ✅ الگوی جدید
// UserId = 0 → بدون فیلتر (نمایش همه) — مناسب Admin
// UserId > 0 → فیلتر بر اساس کاربر خاص — مناسب Customer یا Admin
public async Task<Result> Handle(SomeQuery request, CancellationToken ct)
{
var userId = request.UserId;
var query = _context.SomeEntity.AsNoTracking();
if (userId > 0)
query = query.Where(x => x.UserId == userId);
// userId == 0 → no filter → return all
return await query.ToListAsync(ct);
}
```
### الگوی Customer Service (JWT رو خودش resolve میکنه):
```csharp
// ✅ Customer endpoint → حتماً JWT resolve میکنه
public override async Task<Response> GetMyData(Request request, ServerCallContext context)
{
if (!long.TryParse(_currentUserService.UserId, out var userId) || userId == 0)
throw new RpcException(new Status(StatusCode.Unauthenticated, "User not authenticated"));
var query = new GetDataQuery { UserId = userId }; // ← userId صریح
var result = await _sender.Send(query, context.CancellationToken);
return MapToResponse(result);
}
```
### الگوی Admin Service (UserId رو از request میگیره):
```csharp
// ✅ Admin endpoint → UserId از request (0 = همه)
public override async Task<Response> GetAllData(Request request, ServerCallContext context)
{
// request.UserId = 0 → handler همه رو برمیگردونه
// request.UserId > 0 → handler فیلتر میکنه
var result = await _dispatcher.Send(request, context);
return result;
}
```
---
## 📝 لیست تغییرات
### 🔧 ۸ Query Handler اصلاح‌شده:
| # | Handler | تغییر | رفتار `UserId = 0` |
|---|---------|-------|---------------------|
| 1 | `GetCustomerOrdersQueryHandler` | حذف `ICurrentUserService` + JWT fallback | بدون فیلتر → همه سفارشات |
| 2 | `GetCustomerOrderQueryHandler` | حذف `ICurrentUserService` + JWT fallback | هر سفارشی با OrderId |
| 3 | `GetUserWeeklyBalancesQueryHandler` | حذف `ICurrentUserService` + JWT fallback | بدون فیلتر → همه تعادل‌ها |
| 4 | `GetUserCommissionPayoutsQueryHandler` | حذف `ICurrentUserService` + JWT fallback | بدون فیلتر → همه پرداخت‌ها |
| 5 | `GetNetworkStatisticsQueryHandler` | حذف `ICurrentUserService` + JWT fallback | آمار root user (کل شبکه) |
| 6 | `GetNetworkTreeQueryHandler` | حذف JWT fallback + خطا اگر UserId نباشد | `ArgumentException` (الزامی) |
| 7 | `GetUserQueryHandler` | حذف JWT fallback + خطا اگر UserId نباشد | `ArgumentException` (الزامی) |
| 8 | `GetUserWalletQueryHandler` | حذف JWT fallback + خطا اگر UserId نباشد | `ArgumentException` (الزامی) |
### 🌐 ۴ Customer Service Endpoint اصلاح‌شده:
| # | Service / Method | تغییر |
|---|-----------------|-------|
| 1 | `UserOrderService.GetCustomerOrders` | JWT resolve → ارسال `customerUserId` به handler |
| 2 | `UserOrderService.GetCustomerOrder` | JWT resolve → ارسال `customerUserId` به handler |
| 3 | `NetworkMembershipService.GetMyNetworkStatistics` | افزودن `ICurrentUserService` + JWT resolve |
| 4 | `UserWalletService.GetCustomerWallet` | تغییر از `Id = 0` به `Id = userId` (از JWT) |
---
## 📐 دیاگرام جریان
### درخواست Admin (BackOffice):
```
BackOffice Panel → gRPC (UserId=0) → Admin Service → Handler (UserId=0 → no filter → ALL users) ✅
BackOffice Panel → gRPC (UserId=42) → Admin Service → Handler (UserId=42 → filter → one user) ✅
```
### درخواست Customer (FrontOffice):
```
FrontOffice App → gRPC → Customer Service → JWT resolve (UserId=42) → Handler (UserId=42 → filter) ✅
```
---
## ⚠️ نکات مهم
1. **Handler ها هرگز `ICurrentUserService` رو inject نمیکنند** (بعد از این فیکس)
2. فقط **Customer Service endpoints** مسئول JWT resolve هستند
3. **Admin endpoints** از `IDispatchRequestToCQRS` استفاده میکنند و UserId مستقیم از proto request میاد
4. Handler هایی که UserId **الزامی** دارند (مثل GetUser, GetUserWallet, GetNetworkTree) → `ArgumentException` پرتاب میکنند
5. Handler هایی که لیست برمیگردونند (مثل GetCustomerOrders, GetWeeklyBalances) → `UserId = 0` یعنی "بدون فیلتر"
---
## 🔗 فایل‌های تغییر‌یافته
### Application Layer:
```
CMS/src/CMSMicroservice.Application/
├── OrdersCQ/Queries/GetCustomerOrders/GetCustomerOrdersQueryHandler.cs
├── OrdersCQ/Queries/GetCustomerOrder/GetCustomerOrderQueryHandler.cs
├── UserWeeklyBalanceCQ/Queries/GetUserWeeklyBalances/GetUserWeeklyBalancesQueryHandler.cs
├── CommissionPayoutCQ/Queries/GetUserCommissionPayouts/GetUserCommissionPayoutsQueryHandler.cs
├── NetworkStatisticsCQ/Queries/GetNetworkStatistics/GetNetworkStatisticsQueryHandler.cs
├── NetworkTreeCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs
├── UserCQ/Queries/GetUser/GetUserQueryHandler.cs
└── UserWalletCQ/Queries/GetUserWallet/GetUserWalletQueryHandler.cs
```
### WebApi Layer:
```
CMS/src/CMSMicroservice.WebApi/Services/
├── UserOrderService.cs (GetCustomerOrders + GetCustomerOrder)
├── NetworkMembershipService.cs (GetMyNetworkStatistics)
└── UserWalletService.cs (GetCustomerWallet)
```
-131
View File
@@ -1,131 +0,0 @@
# پلن حذف BFF‌ها — اتصال مستقیم فرانت‌اند به CMS
> تاریخ: February 10, 2026
---
## 📊 خلاصه وضعیت
### یافته‌های کلیدی:
1. **هر دو فرانت (BackOffice + FrontOffice) الان از proto‌های CMS مستقیم استفاده میکنن** — مهاجرت proto انجام شده
2. **CMS خودش `VerifyOtpToken` و `AcceptContract` composite handler داره** — فقط یه `TODO` در AcceptContract برای JWT generation
3. **CMS خودش `IPaymentGatewayService` + `DayaPaymentService` داره** — PYMS جداگانه لازم نیست
4. **CMS خودش Kavenegar + SignalR Hub داره** — آماده‌ست
5. **CMS خودش `ICurrentUserService` داره** — Security logic آماده‌ست
---
## فاز ۱ — حذف BackOffice.BFF ✅ (انجام میشه الان)
### ✅ تسک ۱.۱ — Permission Interceptor (انتقال)
**وضعیت**: ✅ **انجام شد**
**چیزی که هست (BFF)**:
- `RequiresPermissionAttribute` — Attribute برای mark کردن gRPC methods
- `PermissionInterceptor` — gRPC interceptor که attribute ها رو چک میکنه
- `IPermissionService` + `PermissionService` — Role رو از JWT میخونه
- `RolePermissionConfig` — ماتریس Role→Permission (3 نقش × 34 permission)
**نقش‌ها**: SuperAdmin (Administrator), Admin, Inspector
**مجوزها**: 34 مجوز در 9 دسته (Dashboard, Orders, Products, Users, Commission, PublicMessages, ManualPayments, Settings, Reports)
**فایل‌های ساخته شده:**
- `Application/Common/Authorization/RequiresPermissionAttribute.cs`
- `Application/Common/Authorization/PermissionDefinitions.cs`
- `Application/Common/Authorization/IPermissionService.cs`
- `Infrastructure/Services/Authorization/PermissionService.cs`
- `WebApi/Interceptors/PermissionInterceptor.cs`
**Attribute‌های اضافه شده (۲۱ عدد بر روی ۴ سرویس):**
- `AppVersionService`: GetAppVersion(settings.view), GetAllAppVersions(settings.view), UpdateAppVersion(settings.manage_configuration)
- `ConfigurationService`: GetAllConfigurations(settings.view), CreateOrUpdateConfiguration(settings.manage_configuration), DeactivateConfiguration(settings.manage_configuration)
- `ManualPaymentService`: CreateManualPayment(manualpayments.create), ApproveManualPayment(manualpayments.approve), RejectManualPayment(manualpayments.approve), GetAllManualPayments(manualpayments.view), ProcessManualMembershipPayment(manualpayments.create)
- `UserOrderService`: CreateNewUserOrder(orders.create), UpdateUserOrder(orders.update), DeleteUserOrder(orders.delete), GetUserOrder(orders.view), GetAllUserOrderByFilter(orders.view), UpdateOrderStatus(orders.update), GetOrdersByDateRange(reports.view), ApplyDiscountToOrder(orders.update), CalculateOrderPV(orders.view), CancelOrder(orders.cancel)
### ❌ تسک ۱.۲ — AfrinoIDP OTP (بعداً)
**وضعیت**: **پلن شده — فعلاً نیاز نیست**
BackOffice ادمین لاگین از طریق `https://ids.afrino.co` (AfrinoIDP) انجام میشه.
این یه external identity provider هست — فرانت BackOffice خودش مستقیم با AfrinoIDP ارتباط داره (OIDC flow).
CMS فقط JWT رو validate میکنه — نیازی به proxy نداره.
### ✅ تسک ۱.۳ — تغییر GwUrl
**وضعیت**: ✅ **نیاز نبود — قبلاً انجام شده بود**
| فایل | از | به |
|------|-----|-----|
| `BackOffice/wwwroot/appsettings.json` | `https://localhost:32846` | `https://localhost:32846` (بدون تغییر — dev) |
| `BackOffice/wwwroot/appsettings.Staging.json` | ✅ **قبلاً** `https://cms.se.kbs1.ir` | بدون تغییر |
> BackOffice Staging **قبلاً مستقیم به CMS وصله!** فقط dev (localhost) هنوز BFF روی همون پورته.
---
## فاز ۲ — حذف FrontOffice.BFF ✅ (انجام میشه الان)
### ✅ تسک ۲.۱ — Kavenegar SMS
**وضعیت**: ✅ **قبلاً در CMS هست**`IKavenegarService` + `KavenegarService`
### ✅ تسک ۲.۲ — SignalR Token Relay
**وضعیت**: ✅ **انجام شد** — آلیاس `/hubs/token-relay` در CMS اضافه شد
**وضعیت فعلی**:
- CMS Hub: `/hubs/token-notification` (اصلی ✅)
- CMS Hub: `/hubs/token-relay` (آلیاس برای backward compatibility ✅)
- FrontOffice Staging: `HubPath``/hubs/token-notification`
### ✅ تسک ۲.۳ — VerifyOtp + AcceptContract composite
**وضعیت**: ✅ **کامل شد**
- `VerifyOtpTokenCommandHandler` — OTP verify + JWT generation ✅
- `AcceptContractCommandHandler` — Contract create + OTP verify + JWT generation ✅ (TODO فیکس شد → `IGenerateJwtToken` واقعی)
### ✅ تسک ۲.۴ — PYMS (Zarinpal Payment)
**وضعیت**: ✅ **نیاز نیست**
**دلیل**: FrontOffice **الان از CMS `TransactionsContract.CustomerPaymentRequest/Verification` استفاده میکنه** — مستقیم PYMS صدا نمیزنه.
CMS هم از `IPaymentGatewayService` (DayaPaymentService) برای payment استفاده میکنه.
PYMS فقط در BFF بود — فرانت هیچوقت مستقیم PYMS صدا نمیزنه.
### ✅ تسک ۲.۵ — Security Logic (currentUserId injection)
**وضعیت**: ✅ **قبلاً در CMS هست**
CMS `ICurrentUserService` رو inject میکنه و `GetCurrentUserId()` helper در همه Customer سرویس‌ها هست:
- UserService ✅
- UserOrderService ✅
- UserWalletService ✅
- TransactionsService ✅
- PackageService ✅
- ClubMembershipService ✅
- ConfigurationService ✅
### ✅ تسک ۲.۶ — تغییر GwUrl FrontOffice
**وضعیت**: ✅ **انجام شد**
| فایل | از | به |
|------|-----|-----|
| `FrontOffice/appsettings.Staging.json` | `https://frontoffice-bff.se.kbs1.ir` | ✅ `https://cms.se.kbs1.ir` |
| `FrontOffice/appsettings.Staging.json` HubPath | `/hubs/token-relay` | ✅ `/hubs/token-notification` |
| `FrontOffice/appsettings.json` | `https://localhost:32846` | بدون تغییر (dev) |
---
## خلاصه کارهای واقعی
### ✅ همه تسک‌ها انجام شد:
1. ✅ Permission Interceptor infrastructure + DI + gRPC pipeline
2.`[RequiresPermission]` attributes روی ۲۱ endpoint در ۴ سرویس BackOffice
3. ✅ فیکس `TODO` JWT generation در AcceptContractCommandHandler
4. ✅ تغییر GwUrl FrontOffice Staging → `cms.se.kbs1.ir`
5. ✅ مپ `/hubs/token-relay` → آلیاس در CMS (backward compatibility)
6. ✅ Kavenegar — قبلاً در CMS بود
7. ✅ Security Logic (ICurrentUserService) — قبلاً در CMS بود
8. ✅ VerifyOtp/AcceptContract composite — قبلاً در CMS بود + JWT فیکس شد
9. ✅ PYMS — فرانت مستقیم CMS protos استفاده میکنه
### ❌ نیاز نیست:
1. AfrinoIDP — BackOffice خودش OIDC flow مستقیم داره
### 📋 تست‌های لازم قبل از حذف نهایی BFF:
1. BackOffice Staging → اتصال مستقیم به CMS + تست Permission Interceptor
2. FrontOffice Staging → اتصال به `cms.se.kbs1.ir` + تست SignalR + تست OTP/AcceptContract
-445
View File
@@ -1,445 +0,0 @@
# CMS Microservice - Network & Club Commission + Inventory Management System
[![Status](https://img.shields.io/badge/Status-Active%20Development-success)]()
[![Progress](https://img.shields.io/badge/Inventory%20System-Phase%202%20Complete-blue)]()
[![Phase](https://img.shields.io/badge/Next-Business%20Services-orange)]()
## 📊 Project Status (January 2026)
### 🏪 Inventory Management System - NEW!
**Progress**: Phase 2 Complete (50%)
**Architecture**: Clean Architecture + CQRS + Repository Pattern
#### ✅ Completed Phases
1.**Phase 1: Infrastructure & Domain Layer**
- Domain Entities: `InventoryItem`, `StockMovement`, `Warehouse`
- Domain Enums: `StockMovementType`
- EF Core Configurations with proper indexing
- Database migration applied
2.**Phase 2: Repository Pattern & CQRS**
- Repository Interfaces & Implementations
- CQRS Commands (17 commands)
- CQRS Queries (35 queries)
- MediatR Handlers (52 handlers)
#### 🔄 In Progress
3. 🔄 **Phase 3: Business Services Layer**
4.**Phase 4: DTOs & AutoMapper**
5.**Phase 5: API Controllers**
---
### 💼 Commission System - Production Ready
**Progress**: 85% Complete
**MVP Status**: ✅ 100% Complete
#### ✅ Completed Features
- ✅ Binary network tree with automatic placement
- ✅ Club membership (Member/Trial) with commission rates
- ✅ Weekly commission calculation (Lesser Leg algorithm)
- ✅ Background worker with Hangfire
- ✅ Email + SMS notifications (MailKit + Kavenegar)
- ✅ Health check endpoints (Kubernetes-ready)
### 🟡 Partially Complete
- Phase 10: Withdrawal & Settlement (40%)
- ✅ Commands & Database
- ❌ Payment Gateway Integration
### ❌ Not Started
- Phase 9: Club Shop & Product Integration (0%)
---
## 🚀 Recent Updates (January 2026)
### 🏪 Inventory Management System - NEW! ✅
**Complete CQRS-based inventory management with:**
#### Domain Layer:
-`InventoryItem` - Multi-warehouse product tracking with min/max thresholds
-`StockMovement` - Complete audit trail with 8 movement types
-`Warehouse` - Multi-location support with default warehouse
#### Repository Pattern:
-`IInventoryItemRepository` - 25+ methods for inventory operations
-`IStockMovementRepository` - Movement tracking & analytics
-`IWarehouseRepository` - Warehouse management & statistics
#### CQRS Commands (17 total):
- **Inventory:** Create, Update, Delete, Reserve, Release, Reduce, Increase
- **Movement:** Create, BulkCreate, Delete
- **Warehouse:** Create, Update, Delete, SetDefault, Activate, BulkCreate
#### CQRS Queries (35 total):
- **Inventory:** GetById, Search, LowStock, OutOfStock, CheckAvailability
- **Movement:** GetHistory, GetByOrder, Search, Analytics, DailyVolume, TopMoving
- **Warehouse:** GetById, Search, GetStats, GetLowStock, GetAllStats
#### Business Features:
- ✅ Multi-warehouse inventory management
- ✅ Stock reservation system for orders
- ✅ Automatic movement tracking
- ✅ Low stock & out-of-stock alerts
- ✅ Advanced analytics & reporting
- ✅ Bulk operations support
- ✅ Transaction-safe operations
---
### Email & SMS Notifications - COMPLETED ✅
-**MailKit 4.14.1** for Email (SMTP with HTML templates)
-**Kavenegar 1.2.5** for SMS (Iranian SMS gateway)
- ✅ User.Email field added with migration
- ✅ 3 notification types: Commission, Club activation, Errors
- ✅ Persian RTL templates with rich formatting
- ✅ Production configuration guide created
### Hangfire Job Scheduling - COMPLETED ✅
- ✅ Dashboard UI at `/hangfire`
- ✅ Cron schedule: Sunday 00:05 UTC
- ✅ SQL Server persistence
- ✅ Manual trigger API endpoints
- ✅ Distributed execution support
### Infrastructure Enhancements - COMPLETED ✅
- ✅ Health Check endpoints (`/health`, `/health/ready`, `/health/live`)
- ✅ AlertService (structured logging for Sentry/Slack)
- ✅ Retry logic (Polly 8.5.0 with exponential backoff)
- ✅ WorkerExecutionLog (database audit trail)
- ✅ CurrentUserService (JWT authentication context)
---
## 🏗️ Architecture
**Clean Architecture** with 4 layers:
```
CMSMicroservice.Domain/ # Entities, Enums, Interfaces
├── Entities/
│ ├── InventoryItem.cs # NEW: Inventory tracking
│ ├── StockMovement.cs # NEW: Movement audit
│ └── Warehouse.cs # NEW: Multi-warehouse
├── Enums/
│ └── StockMovementType.cs # NEW: Movement types
CMSMicroservice.Application/ # CQRS (Commands, Queries, MediatR)
├── Features/
│ ├── InventoryItems/ # NEW: Inventory CQRS
│ │ ├── Commands/
│ │ ├── Queries/
│ │ └── Handlers/
│ ├── StockMovements/ # NEW: Movement CQRS
│ │ ├── Commands/
│ │ ├── Queries/
│ │ └── Handlers/
│ └── Warehouses/ # NEW: Warehouse CQRS
│ ├── Commands/
│ ├── Queries/
│ └── Handlers/
└── Common/Interfaces/
└── Repositories/ # NEW: Repository interfaces
CMSMicroservice.Infrastructure/ # DbContext, Services, Background Jobs
├── Persistence/
│ ├── Context/
│ ├── Configurations/ # NEW: EF Core configs
│ ├── Repositories/ # NEW: Repository implementations
│ └── Migrations/
└── DependencyInjection.cs # NEW: DI setup
CMSMicroservice.WebApi/ # gRPC Services, Controllers
CMSMicroservice.Protobuf/ # Protocol Buffers definitions
```
**Technology Stack**:
- .NET 9.0
- Entity Framework Core 9.0.11
- gRPC + JSON Transcoding
- Hangfire 1.8.22 (Job Scheduling)
- MediatR 13.0.0 (CQRS)
- Polly 8.5.0 (Resilience)
- MailKit 4.14.1 (Email)
- Kavenegar 1.2.5 (SMS)
- SQL Server
---
## 📖 Documentation
- **[Development Plan](docs/development-plan.md)** - NEW: Inventory system roadmap
- **[Implementation Progress](docs/implementation-progress.md)** - Detailed phase-by-phase progress
- **[Email/SMS Configuration Guide](docs/email-sms-configuration-guide.md)** - Production setup instructions
- **[Balance Calculation Logic](docs/balance-calculation-carryover-logic.md)** - Commission algorithm details
- **[Binary Tree Registration](docs/binary-tree-registration-guide.md)** - Network tree guide
- **[Network Club Commission System](docs/network-club-commission-system-v1.1.md)** - Full system specification
---
## 🏪 Inventory System Usage
### Create Warehouse
```csharp
await mediator.Send(new CreateWarehouseCommand
{
Name = "Main Warehouse",
Code = "WH-001",
IsDefault = true,
IsActive = true
});
```
### Create Inventory Item
```csharp
await mediator.Send(new CreateInventoryItemCommand
{
ProductId = 1,
WarehouseId = 1,
Quantity = 100,
MinQuantity = 10,
MaxQuantity = 1000
});
```
### Reserve Stock for Order
```csharp
await mediator.Send(new ReserveInventoryCommand
{
Id = inventoryId,
Quantity = 5,
OrderId = 12345
});
```
### Check Availability
```csharp
bool available = await mediator.Send(
new CheckInventoryAvailabilityQuery(inventoryId, 10));
```
### Get Low Stock Alerts
```csharp
var lowStock = await mediator.Send(new GetLowStockItemsQuery
{
WarehouseId = 1,
Count = 50
});
```
### Get Movement Analytics
```csharp
var summary = await mediator.Send(new GetMovementSummaryQuery
{
FromDate = DateTime.Now.AddDays(-7),
ToDate = DateTime.Now
});
var topProducts = await mediator.Send(new GetTopMovingProductsQuery
{
FromDate = DateTime.Now.AddDays(-30),
ToDate = DateTime.Now,
Count = 10
});
```
---
## 🚀 Quick Start
### Prerequisites
- .NET 9.0 SDK
- SQL Server (local or remote)
- (Optional) Gmail account for Email
- (Optional) Kavenegar account for SMS
### 1. Clone & Build
```bash
cd /home/masoud/Apps/project/FourSat/CMS/src
dotnet build
```
### 2. Configure Database
Update `appsettings.json` with your SQL Server connection:
```json
"ConnectionStrings": {
"DefaultConnection": "Server=YOUR_SERVER;Database=Foursat_CMS;..."
}
```
### 3. Apply Migrations
```bash
cd CMSMicroservice.WebApi
dotnet ef database update
```
### 4. Configure Notifications (Optional)
See [Email/SMS Configuration Guide](docs/email-sms-configuration-guide.md)
### 5. Run
```bash
dotnet run --urls="http://localhost:5133"
```
### 6. Access Endpoints
- **Health**: http://localhost:5133/health
- **Hangfire Dashboard**: http://localhost:5133/hangfire
- **gRPC**: localhost:5133 (HTTP/2)
---
## 🔧 Configuration
### Email (SMTP)
```json
"Email": {
"Enabled": true,
"SmtpHost": "smtp.gmail.com",
"SmtpPort": 587,
"SmtpUsername": "your-email@gmail.com",
"SmtpPassword": "your-gmail-app-password",
"FromEmail": "noreply@foursat.com",
"FromName": "FourSat CMS",
"EnableSsl": true
}
```
### SMS (Kavenegar)
```json
"Sms": {
"Enabled": true,
"Provider": "Kavenegar",
"KavenegarApiKey": "YOUR_API_KEY",
"Sender": "10008663"
}
```
### Background Worker
```csharp
// Cron: "5 0 * * 0" = Every Sunday at 00:05 UTC
RecurringJob.AddOrUpdate<WeeklyCommissionJob>(
"weekly-commission-calculation",
job => job.ExecuteAsync(CancellationToken.None),
"5 0 * * 0");
```
---
## 🧪 Testing
### Manual Trigger (via API)
```bash
# Trigger weekly calculation immediately
curl -X POST http://localhost:5133/api/admin/trigger-weekly-calculation
# Trigger recurring job now
curl -X POST http://localhost:5133/api/admin/trigger-recurring-job-now
# Get recurring jobs status
curl http://localhost:5133/api/admin/recurring-jobs-status
```
### Health Checks
```bash
curl http://localhost:5133/health # Overall health
curl http://localhost:5133/health/ready # Readiness probe (K8s)
curl http://localhost:5133/health/live # Liveness probe (K8s)
```
---
## 📊 What's Remaining?
### 🏪 Inventory System (Current Focus)
1. **Phase 3: Business Services** (In Progress)
- `IInventoryManagementService` - High-level operations
- `IStockMovementService` - Movement orchestration
- `IWarehouseService` - Warehouse business logic
- `IInventoryReportingService` - Advanced reporting
2. **Phase 4: DTOs & AutoMapper** (Next)
- Request/Response DTOs
- AutoMapper profiles
- Validation rules
3. **Phase 5: API Controllers** (Planned)
- `InventoryController` - REST API
- `WarehouseController` - Warehouse management
- `StockMovementController` - Movement tracking
- Swagger documentation
### 💼 Commission System
1. **Payment Gateway Integration** (Phase 10 - 1 week)
- Daya or Bank Mellat API integration
- IBAN transfer automation
- Admin approval UI in BackOffice
2. **Production Configuration** (30 minutes)
- Gmail App Password setup
- Kavenegar API key registration
- Update `appsettings.Production.json`
### Medium Priority
3. **Club Shop Integration** (Phase 9 - 2 weeks)
- Product catalog for club memberships
- Shopping cart integration
- Auto-activation on purchase
### Low Priority
4. **Testing** (Phase 7 - Postponed)
- Unit tests for business logic
- Integration tests for API
- Load testing for background worker
### Optional Enhancements
- Redis distributed locks (multi-server deployment)
- Sentry error tracking (API key needed)
- Slack notifications (webhook needed)
- FCM push notifications
---
## 🎯 MVP Features (100% Complete)
### 💼 Commission System:
✅ Binary network tree with automatic placement
✅ Club membership (Member/Trial) with different commission rates
✅ Weekly commission calculation (Lesser Leg algorithm)
✅ Background worker with Hangfire (cron scheduling)
✅ Balance carryover logic (rollover unused volumes)
✅ MaxWeeklyBalances cap enforcement
✅ Health check endpoints (Kubernetes-ready)
✅ Manual trigger API (admin control)
✅ Email + SMS notifications (MailKit + Kavenegar)
✅ Retry logic with exponential backoff (Polly)
✅ Audit trail (WorkerExecutionLog, History tables)
✅ Structured logging (AlertService for Sentry/Slack)
✅ JWT authentication context (CurrentUserService)
### 🏪 Inventory System (Phase 2 Complete):
✅ Domain entities (InventoryItem, StockMovement, Warehouse)
✅ Multi-warehouse inventory management
✅ Stock reservation system for orders
✅ 8 movement types with complete audit trail
✅ Repository pattern with 25+ methods per repository
✅ CQRS with 17 commands and 35 queries
✅ 52 MediatR handlers with business logic
✅ Low stock and out-of-stock alerts
✅ Advanced analytics (top products, daily volume)
✅ Bulk operations support
✅ Transaction-safe operations with rollback
✅ DI container configuration
---
## 👥 Team
**Development**: FourSat Team
**Last Updated**: January 2026
---
## 📝 License
Proprietary - FourSat Company
# Multi-remote push enabled
-251
View File
@@ -1,251 +0,0 @@
# 📁 معماری مدیریت فایل و تصاویر — CMS
> **تاریخ:** ۱۴۰۴/۱۱/۲۸ (February 17, 2026)
> **وضعیت:** ✅ عملیاتی
> **Build:** 0 Error (هر ۳ پروژه) ✅
---
## ۱. پیش‌زمینه
سیستم قبلی از **FMS (File Management Service)** در آدرس `https://dl.afrino.co` استفاده می‌کرد که غیرقابل دسترس/ناسازگار شده بود. در چندین فاز، معماری فایل‌ها به صورت کامل بازنویسی شد:
| فاز | شرح | وضعیت |
|-----|------|-------|
| ۱. حذف FMS | حذف کامل ۳ فایل مرده FMS | ✅ |
| ۲. حالت base64 | ذخیره data URI مستقیم در DB | ✅ (بازنشسته) |
| ۳. ذخیره دیسکی | فایل در دیسک + مسیر در DB + تبدیل به base64 هنگام serve | ✅ |
| ۴. **سرو HTTP عمومی** | **اندپوینت `/uploads/{path}` + Fallback FMS** | **✅ جدید** |
---
## ۲. معماری نهایی
```
BackOffice (Blazor WASM)
│ MudFileUpload → IBrowserFile → byte[] → gRPC ImageFileModel
CMS gRPC Service
│ proto ImageFileModel → Command.ImageFileBytes
MediatR Handler
│ IFileManager.UploadImageAsync(folder, bytes, mime, name)
LocalFileManager
├─ Main Image → Uploads/Images/{folder}/{guid}.jpg (1200×1200, JPEG Q75)
├─ Thumbnail → Uploads/Images/{folder}/{guid}_thumb.jpg (300×300, JPEG Q75)
│ Returns: { Main.Path, Thumbnail.Path } (relative paths stored in DB)
ImagePathResolverInterceptor (gRPC response)
│ Walks all response fields → reads file from disk → data:{mime};base64,{bytes}
BackOffice / FrontOffice ← receives base64 data URI directly in proto fields
```
---
## ۳. اجزای کلیدی
### ۳.۱ `IFileManager` — Interface
**مسیر:** `Application/Common/FileManager/IFileManager.cs`
```csharp
public interface IFileManager
{
Task<UploadResult> UploadAsync(string folder, byte[] file, string mime, string? fileName, CancellationToken ct);
Task<ImageUploadResult> UploadImageAsync(string folder, byte[] file, string mime, string? fileName, CancellationToken ct);
Task DeleteAsync(string path, CancellationToken ct);
string? ResolveImageUrl(string? path);
}
```
- **`UploadAsync`** — آپلود فایل خام
- **`UploadImageAsync`** — بهینه‌سازی + ساخت thumbnail خودکار (SixLabors.ImageSharp)
- **`ResolveImageUrl`** — تبدیل مسیر نسبی به data URI (base64)
### ۳.۲ `LocalFileManager` — پیاده‌سازی
**مسیر:** `Infrastructure/Services/LocalFileManager.cs`
| ویژگی | مقدار |
|-------|-------|
| ریشه آپلود | `FileStorage:UploadPath` یا `AppContext.BaseDirectory/Uploads` |
| فرمت تصویر اصلی | JPEG, Quality 75, حداکثر 1200×1200 |
| فرمت thumbnail | JPEG, Quality 75, حداکثر 300×300 |
| نام‌گذاری فایل | `{Guid}.jpg` + `{Guid}_thumb.jpg` |
| DI Registration | `services.AddSingleton<IFileManager, LocalFileManager>()` |
### ۳.۳ `ImagePathResolverInterceptor` — gRPC Interceptor
**مسیر:** `WebApi/Interceptors/ImagePathResolverInterceptor.cs`
اینترسپتور **خودکار** تمام فیلدهای تصویری را در response‌های gRPC پیدا کرده و مسیر نسبی را به data URI تبدیل می‌کند.
**فیلدهای شناسایی‌شده:**
- `image_path`, `thumbnail_path`, `image_thumbnail_path`
- `featured_image_path`, `featured_image_thumbnail_path`
- `hero_image_path`, `product_thumbnail_path`
- `avatar_path`, `avatar_url`
**قابلیت‌ها:**
- Walk بازگشتی پیام‌های proto
- پشتیبانی از `string` ساده و `Google.Protobuf.WellKnownTypes.StringValue`
- پشتیبانی از فیلدهای `repeated` (collection‌های تو در تو)
- اگر مقدار `data:` یا `http` باشد → رد می‌شود (تبدیل نمی‌شود)
### ۳.۴ `LoggingBehaviour` — پاکسازی لاگ
**مسیر:** `WebApi/Common/Behaviours/LoggingBehaviour.cs`
- فرمت لاگ: `JsonFormatter.Default.Format()` به جای `{@Request}`
- پاکسازی فیلدهای باینری با regex (`File`, `ImageFile`, `image_file`, `file`)
- محدودیت طول لاگ: حداکثر 2000 کاراکتر
### ۳.۵ `UploadsController` — سرو عمومی فایل‌ها (HTTP) 🆕
**مسیر:** `WebApi/Controllers/UploadsController.cs`
اندپوینت عمومی REST برای سرو مستقیم تصاویر بدون نیاز به base64. مناسب برای بارگذاری تصاویر در تگ `<img>` و کاهش پهنای باند.
| ویژگی | مقدار |
|-------|-------|
| مسیر | `GET /uploads/{**path}` |
| احراز هویت | `[AllowAnonymous]` — عمومی |
| کش مرورگر | `ResponseCache 86400` ثانیه (۲۴ ساعت) |
| Content-Type | تشخیص خودکار از پسوند فایل (`FileExtensionContentTypeProvider`) |
| Range Requests | ✅ فعال (`enableRangeProcessing: true`) |
| محافظت مسیر | جلوگیری از path traversal (`..`, `\`, `Path.GetFullPath` validation) |
**FMS Fallback:**
اگر فایل محلی وجود نداشته باشد و تنظیم `FMS:Address` پر باشد:
1. فایل از `{FMS:Address}/{relativePath}` دانلود می‌شود
2. Content-Type بررسی می‌شود (فقط `image/*` و `application/pdf` مجاز)
3. فایل روی دیسک محلی ذخیره و کش می‌شود
4. سپس فایل محلی سرو می‌شود
```
Client → GET /uploads/Images/BlogPosts/abc.jpg
├─ فایل محلی وجود دارد? → سرو مستقیم از دیسک
└─ فایل محلی وجود ندارد?
└─ FMS:Address تنظیم شده?
├─ بله → دانلود از dl.afrino.co → ذخیره محلی → سرو
└─ خیر → 404 Not Found
```
**وابستگی‌ها:**
- `IHttpClientFactory` با named client `"FMS"` (timeout: 30 ثانیه)
- ثبت در `Program.cs`: `builder.Services.AddHttpClient("FMS", ...)`
---
## ۴. Proto Messages — ImageFileModel
هر حوزه (DiscountProduct, BlogPost, SitePage) پیام مستقل `ImageFileModel` خود را دارد:
### DiscountProduct
```protobuf
message ImageFileModel {
bytes file = 1;
string mime = 2;
string file_name = 3;
}
```
**استفاده در:** `CreateDiscountProductRequest`, `UpdateDiscountProductRequest`
### BlogPost
```protobuf
message BlogImageFileModel {
bytes file = 1;
string mime = 2;
string file_name = 3;
}
```
**استفاده در:** `CreateBlogPostRequest`, `UpdateBlogPostRequest`
### SitePage
```protobuf
message SitePageImageFileModel {
bytes file = 1;
string mime = 2;
string file_name = 3;
}
```
**استفاده در:** `UpdateSitePageRequest`, `CreateSitePageSectionRequest`, `UpdateSitePageSectionRequest`
---
## ۵. جریان آپلود تصویر (مثال: BlogPost)
```
1. کاربر در BackOffice → MudFileUpload → انتخاب فایل
2. BlogPostEditDialog.OnImageSelected()
→ IBrowserFile.OpenReadStream() → byte[] + ContentType + FileName
→ پیش‌نمایش base64 در UI
3. Submit → BlogPostEditDto { ImageFile = bytes, ImageMime, ImageFileName }
4. BlogPostService.CreateAsync()
→ BlogImageFileModel { File = ByteString.CopyFrom(bytes), Mime, FileName }
→ gRPC CreateBlogPostRequest
5. CMS BlogPostService (gRPC) → CreateBlogPostCommand
{ ImageFileBytes = request.ImageFile.File.ToByteArray(), ... }
6. CreateBlogPostCommandHandler.Handle()
→ _fileManager.UploadImageAsync("Images/BlogPosts", bytes, mime, name)
→ post.FeaturedImagePath = result.Main.Path
→ post.FeaturedImageThumbnailPath = result.Thumbnail.Path
7. Response → ImagePathResolverInterceptor
→ featured_image_path → data:image/jpeg;base64,...
→ featured_image_thumbnail_path → data:image/jpeg;base64,...
8. BackOffice / FrontOffice → نمایش مستقیم base64 data URI
```
---
## ۶. فایل‌های حذف‌شده (کد مرده FMS)
| فایل | شرح |
|------|------|
| `Infrastructure/Services/FmsFileManager.cs` | پیاده‌سازی قدیمی FMS (HTTP upload) |
| `Application/Common/FileManager/FileManagementService.cs` | سرویس قدیمی مدیریت فایل |
| `Application/Common/FileManager/IFileManagementService.cs` | اینترفیس قدیمی |
---
## ۷. تنظیمات
### `appsettings.json` (CMS)
```json
{
"FileStorage": {
"UploadPath": "/app/Uploads"
}
}
```
### محدودیت حجم gRPC
```csharp
// Program.cs
services.AddGrpc(o => o.MaxReceiveMessageSize = 50 * 1024 * 1024); // 50MB
```
### FrontOffice — `UrlUtility.GetImageUrl()`
```csharp
public static string GetImageUrl(string? path)
{
if (string.IsNullOrWhiteSpace(path)) return string.Empty;
if (path.StartsWith("data:") || path.StartsWith("http")) return path;
return $"{DownloadUrl?.TrimEnd('/')}/{path.TrimStart('/')}";
}
```
-619
View File
@@ -1,619 +0,0 @@
# FrontOffice to CMS API Compatibility Analysis
**تاریخ:** 6 فوریه 2026
**وضعیت:** در حال بررسی
## خلاصه اجرایی
این سند مقایسه API‌های مورد نیاز FrontOffice با API‌های موجود در CMS را نشان می‌دهد.
---
## 1. User APIs (Authentication & Profile)
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetUser()` | AuthService, Personal.razor | ✅ موجود | `GetUser(GetUserRequest)` |
| `UpdateUser()` | Personal.razor | ✅ موجود | `UpdateUser(UpdateUserRequest)` |
| `RefreshToken()` | AuthService | ✅ موجود | `RefreshToken(RefreshTokenRequest)` |
| `CreateNewOtpToken()` | AuthDialog | ✅ موجود | `CreateNewOtpToken(CreateNewOtpTokenRequest)` |
| `VerifyOtpToken()` | AuthDialog | ✅ موجود | `VerifyOtpToken(VerifyOtpTokenRequest)` |
| `AcceptContract()` | RegisterWizard | ✅ موجود | `AcceptContract(AcceptContractRequest)` |
| `GetCustomerProfile()` | Profile Pages | ✅ موجود | **پیاده شد در Task قبل** |
| `GetCustomerReferrals()` | Tree.razor | ✅ موجود | **پیاده شد در Task قبل** |
| `GetCustomerSettings()` | Settings.razor | ✅ موجود | **پیاده شد در Task قبل** |
| `UpdateCustomerProfile()` | Personal.razor | ✅ موجود | Proto موجود است |
| `ChangeCustomerPassword()` | ChangePassword.razor | ✅ موجود | Proto موجود است |
| `UpdateCustomerSettings()` | Settings.razor | ✅ موجود | Proto موجود است |
**نتیجه:** ✅ تمام User APIs موجود است
---
## 2. Products APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetCustomerProducts()` | ProductService | ✅ موجود | **پیاده شد در Task قبل** |
| `GetCustomerProductsByFilter()` | ProductService | ✅ موجود | **پیاده شد در Task قبل** |
| `GetAllProductsByFilter()` | Products.razor | ✅ پیاده شد | **Public API - Feb 6, 2026** |
**GetAllProductsByFilter Details:**
- از `GetCustomerProductsByFilterQuery` استفاده می‌کند
- پشتیبانی از فیلترها: Title, Price, Discount, CategoryId, SaleCount, و...
- Sorting: پشتیبانی کامل (مثلاً "price desc")
- Pagination: با MetaData کامل
- CategoryIds: لیست شناسه دسته‌بندی‌های محصول
**نتیجه:** ✅ تمام Products APIs موجود و پیاده شده
---
## 3. Category APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetAllCategoriesForCustomer()` | CategoryService | ✅ پیاده شد | **Customer API - Feb 6, 2026** |
| `GetCategoryById()` | CategoryService | ✅ موجود | Admin API: `GetCategory()` |
**GetAllCategoriesForCustomer Details:**
- از `GetAllCategoryByFilterQuery` استفاده می‌کند
- فقط دسته‌بندی‌های فعال (IsActive = true)
- مرتب‌سازی بر اساس SortOrder
- پشتیبانی Pagination (default: PageSize=100)
- شامل: Id, Name, Title, Description, ImagePath, ParentId, IsActive, SortOrder
- ISender به CategoryService اضافه شد
**نتیجه:** ✅ تمام Category APIs پیاده شده
---
## 4. UserOrder APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetAllUserOrderByFilter()` | OrderService, Orders.razor | ✅ پیاده شد | **Feb 6, 2026** - Admin API |
| `GetUserOrder()` | OrderService, OrderDetail.razor | ✅ پیاده شد | **Feb 6, 2026** - جزئیات کامل سفارش |
| `GetCustomerOrders()` | OrderService | ✅ موجود | Customer API با فیلتر UserId |
| `GetCustomerOrder()` | OrderService | ✅ موجود | Customer API با فیلتر UserId |
| `GetUserOrderHistory()` | OrderService | ✅ موجود | Proto: `GetCustomerOrderHistory()` |
| `GetVATRate()` | VATService, OrderService | ✅ پیاده شد | **Feb 6, 2026** |
| `SubmitShopBuyOrder()` | CheckoutSummary.razor | ✅ پیاده شد | **Feb 6, 2026** - تکمیل فرآیند خرید |
**GetVATRate Details:**
- نرخ مالیات بر ارزش افزوده ایران: 9%
- `VatRate = 0.09` (decimal)
- `VatPercentage = 9` (int)
- `IsEnabled = true`
- استفاده در VATService برای محاسبه مالیات محصولات
**SubmitShopBuyOrder Details (Feb 6, 2026 - Updated with Wallet Payment):**
تبدیل سبد خرید به سفارش نهایی با پرداخت از کیف پول:
1. **احراز هویت**: استخراج UserId از JWT Token (ICurrentUserService)
2. **اعتبارسنجی سبد خرید**:
- بازیابی محصولات سبد خرید با Include(Product)
- چک کردن خالی نبودن سبد
3. **اعتبارسنجی آدرس**:
- دریافت آدرس پیش‌فرض کاربر
- اجباری بودن وجود آدرس
4. **محاسبات مالی**:
- مبلغ پایه: جمع (قیمت × تعداد) تمام آیتم‌ها
- مالیات: 9% از مبلغ پایه
- مبلغ کل: مبلغ پایه + مالیات
- اعتبارسنجی مبلغ: |serverTotal - clientTotal| < 100
5. **اعتبارسنجی کیف پول (New - Feb 6)**:
- بازیابی کیف پول کاربر (UserWallet)
- چک موجودی: Balance >= TotalAmount
- خطا در صورت کمبود موجودی با نمایش موجودی فعلی و مبلغ مورد نیاز
6. **ایجاد تراکنش (New - Feb 6)**:
- Type: TransactionType.Buy (0)
- Amount: TotalAmount
- PaymentStatus: Success
- PaymentDate: DateTime.UtcNow
- RefId: SHOP_{timestamp}
- Description: "خرید محصولات - سفارش #{OrderId}"
7. **کسر از کیف پول (New - Feb 6)**:
- Balance -= TotalAmount
- ثبت موجودی جدید در UserWallet
8. **لاگ تغییرات کیف پول (New - Feb 6)**:
- CurrentBalance: موجودی جدید
- ChangeValue: -TotalAmount (منفی برای برداشت)
- CurrentNetworkBalance: بدون تغییر
- CurrentDiscountBalance: بدون تغییر
- IsIncrease: false (برداشت)
- RefrenceId: TransactionId
9. **ایجاد سفارش (UserOrder) - Updated**:
- TransactionId: لینک به تراکنش (New)
- PaymentStatus: Success (Changed from Pending)
- PaymentDate: DateTime.UtcNow (New)
- PaymentMethod: Wallet (New)
- DeliveryStatus: Pending
- HasVAT: true
10. **ثبت مالیات (OrderVAT)**:
- VATRate: 0.09m (decimal)
- BaseAmount: مبلغ قبل از مالیات
- VATAmount: مبلغ مالیات
- TotalAmount: مبلغ کل
11. **جزئیات فاکتور (FactorDetails)**:
- یک رکورد برای هر آیتم سبد خرید
- ذخیره ProductId, Count, UnitPrice, UnitDiscountPrice
12. **پاکسازی سبد خرید**:
- Soft delete تمام آیتم‌های سبد (IsDeleted = true)
**Transaction Flow:**
```
User → Cart → SubmitShopBuyOrder →
1. Validate Cart
2. Validate Address
3. Calculate Amount (Base + 9% VAT)
4. Validate Wallet Balance
5. Create Transaction (Type=Buy, Status=Success)
6. Deduct from Wallet.Balance
7. Create UserWalletChangeLog (audit trail)
8. Create Order (linked to Transaction, PaymentStatus=Success, PaymentMethod=Wallet)
9. Create OrderVAT
10. Create FactorDetails
11. Clear Cart
→ Return OrderId
```
**Wallet Types:**
- **Balance** (موجودی عادی): Used for purchases - deducted in this flow
- **NetworkBalance** (موجودی شبکه): Commission wallet - not touched
- **DiscountBalance** (موجودی تخفیف): Discount-only wallet - not touched
**Error Handling:**
- "کیف پول یافت نشد": User has no wallet record
- "موجودی کیف پول کافی نیست. موجودی: X تومان، مورد نیاز: Y تومان": Insufficient funds
**خروجی**: شناسه سفارش (OrderId) برای redirect به صفحه جزئیات
**GetUserOrder Details (Feb 6, 2026):**
نمایش جزئیات کامل یک سفارش:
- اطلاعات سفارش: Id, Amount, PaymentStatus, PaymentDate, DeliveryStatus
- اطلاعات کاربر: UserFullName, UserNationalCode
- آدرس: UserAddressText
- مالیات (OrderVAT): VATRate, BaseAmount, VATAmount, TotalAmount, IsPaid
- ردیابی: TrackingCode, DeliveryDescription
- محصولات (FactorDetails): ProductId, ProductTitle, ProductThumbnailPath, UnitPrice, Count, UnitDiscountPrice
**اصلاحات صفحه OrderDetail.razor:**
- ✅ رفع NullReferenceException برای PaymentDate
- ✅ نمایش "تاریخ ثبت" برای سفارشات Pending (بدون PaymentDate)
- ✅ رفع نمایش اشتباه ProductThumbnailPath به جای ProductTitle
- ✅ رفع خطاهای nullable value access (.Value → ?? 0)
- ✅ محاسبه صحیح subtotal با nullable handling
**GetAllUserOrderByFilter Details (Feb 6, 2026):**
لیست تمام سفارشات با فیلترهای پیشرفته:
- فیلترها: UserId (optional - 0 = همه کاربران), PaymentStatus, DeliveryStatus, PaymentDate
- Pagination: MetaData کامل
- Sorting: بر اساس فیلدهای مختلف
- جزئیات هر سفارش: اطلاعات کاربر، آدرس، مالیات، محصولات، وضعیت ارسال
**نتیجه:** ✅ تمام UserOrder APIs پیاده شده - فرآیند خرید کامل است
---
## 5. UserWallet APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetCustomerWallet()` | WalletService | ✅ موجود | 3 نوع کیف پول: Balance, NetworkBalance, DiscountBalance |
| `GetCustomerWalletChangeLog()` | WalletService | ✅ موجود | 6 فیلد موجودی: Current+Change برای هر 3 کیف پول |
| `CustomerWithdrawBalance()` | WalletService | ✅ موجود | Proto موجود است |
| `GetCustomerWithdrawals()` | WithdrawalRequests.razor | ✅ موجود | لیست درخواست‌های برداشت |
| `GetCustomerWithdrawalSettings()` | WalletService | ✅ موجود | حداقل مبلغ برداشت |
**سه نوع کیف پول:**
1. **عادی (Regular)**: Balance & ChangeValue - برای خرید و شارژ عادی
2. **شبکه (Network)**: NetworkBalance & ChangeNerworkValue - پاداش تیمی و کمیسیون
3. **تخفیفی (Discount)**: DiscountBalance & ChangeDiscountValue - برای خرید تخفیفی
**ساختار تراکنش (CustomerWalletChangeLogModel):**
- `CurrentBalance` + `ChangeValue` - موجودی و تغییر کیف پول عادی
- `CurrentNetworkBalance` + `ChangeNerworkValue` - موجودی و تغییر کیف پول شبکه
- `CurrentDiscountBalance` + `ChangeDiscountValue` - موجودی و تغییر کیف پول تخفیفی
- `IsIncrease` - آیا افزایش است یا کاهش
- `RefrenceId` - شناسه ارجاع (سفارش، پرداخت، و...)
- `CreatedAt` - تاریخ تراکنش (UTC Timestamp)
**UI تراکنش‌ها:**
- Desktop: جدول با ستون‌های جداگانه برای هر 3 کیف پول (تغییرات/مانده)
- Mobile: کارت‌ها با 3 باکس افقی (عادی آبی، شبکه سبز، تخفیفی زرد)
- تاریخ: تبدیل UTC به Local Time و نمایش جلالی
- توضیحات: نمایش اینکه کدام کیف پول‌ها تغییر کرده‌اند
**نتیجه:** ✅ تمام UserWallet APIs موجود و پیاده شده با UI کامل (Feb 5, 2026)
---
## 6. Transaction APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetCustomerTransaction()` | TransactionService (در BFF) | ✅ موجود | **پیاده شد در Task قبل** |
| `GetCustomerTransactionsByFilter()` | TransactionService | ✅ موجود | **پیاده شد در Task قبل** |
| `CustomerPaymentRequest()` | Checkout workflow | ✅ موجود | Proto موجود است |
| `CustomerPaymentVerification()` | PaymentCallback.razor | ✅ موجود | Proto موجود است |
**نتیجه:** ✅ تمام Transaction APIs موجود است
---
## 7. UserCarts APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetCustomerCart()` | CartService | ✅ پیاده شد | **Query Handler تکمیل شد - Feb 5** |
| `AddToCustomerCart()` | CartService | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
| `UpdateCustomerCartItem()` | CartService | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
| `RemoveFromCustomerCart()` | CartService | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
**اصلاحات Feb 6, 2026:**
-**رفع باگ Cart APIs در CheckoutSummary**: تمام صفحات از Admin APIs استفاده می‌کردند
- ✅ تغییر `AddNewUserCartAsync``AddNewUserCartForCustomerAsync`
- ✅ تغییر `UpdateUserCartAsync``UpdateUserCartForCustomerAsync`
- ✅ تغییر request model: `AddNewUserCartRequest``AddNewUserCartForCustomerRequest`
- ✅ تغییر request model: `UpdateUserCartRequest``UpdateUserCartForCustomerRequest`
- ✅ اضافه `RemoveUserCartForCustomerAsync` برای حذف صحیح آیتم
- ✅ اصلاح field name: `UserCartId``CartItemId` (Proto: cart_item_id)
- ✅ رفع منطق حذف: از Update با Count=0 به RemoveUserCartForCustomer تغییر یافت
**Field Naming Convention:**
- Proto: `cart_item_id` (snake_case)
- C# Generated: `CartItemId` (PascalCase)
- ❌ نباید: `UserCartId` (نام قدیمی Admin API)
**تاثیر:** حالا عملیات سبد خرید (افزودن/ویرایش/حذف) صحیح کار می‌کند و فقط سبد کاربر جاری را تغییر می‌دهد
**نتیجه:** ✅ تمام UserCart Customer APIs پیاده شده و باگ‌های Security و Field Naming رفع شد
---
## 8. UserAddress APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetCustomerAddresses()` | Addresses.razor | ✅ پیاده شد | **Query Handler تکمیل شد - Feb 5** |
| `CreateCustomerAddress()` | AddAddressDialog.razor | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
| `UpdateCustomerAddress()` | EditAddressDialog.razor | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
| `DeleteCustomerAddress()` | Addresses.razor | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
| `SetCustomerDefaultAddress()` | Addresses.razor | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
**یادداشت:** CityName و ProvinceName در response خالی است - FrontOffice باید از City API جداگانه استفاده کند.
**اصلاحات Feb 6, 2026:**
-**رفع باگ صفحه Addresses**: تمام صفحات FrontOffice از Admin APIs استفاده می‌کردند
- ✅ تغییر `GetAllUserAddressByFilter``GetCustomerAddresses` در Addresses.razor
- ✅ تغییر `CreateNewUserAddress``CreateCustomerAddress` در AddAddressDialog
- ✅ تغییر `UpdateUserAddress``UpdateCustomerAddress` در EditAddressDialog
- ✅ تغییر `DeleteUserAddress``DeleteCustomerAddress` در Addresses.razor
- ✅ تغییر `SetAddressAsDefault``SetCustomerDefaultAddress` در Addresses.razor
- ✅ اصلاح Model type: `GetAllUserAddressByFilterResponseModel``CustomerAddressModel`
- ✅ اصلاح field name: `response.Addresses``response.Models`
**تاثیر:** حالا کاربران فقط آدرس‌های خودشان را می‌بینند (قبلاً همه آدرس‌ها نمایش داده می‌شد)
**نتیجه:** ✅ تمام UserAddress Customer APIs پیاده شده و باگ Security رفع شد
---
## 9. City APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetAllCities()` | AddressDialog components | ✅ موجود | Public API |
**نتیجه:** ✅ City APIs موجود است
---
## 10. Package APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetCustomerPackages()` | PackageService | ✅ موجود | **پیاده شد در Task قبل** |
| `GetCustomerPackageDetails()` | PackageService | ✅ موجود | **پیاده شد در Task قبل** |
| `CustomerPurchasePackage()` | Package purchase flow | ✅ موجود | Proto موجود است |
| `CustomerVerifyPackagePurchase()` | Package verification | ✅ موجود | Proto موجود است |
| `GetCustomerPurchaseHistory()` | MyPackages.razor | ✅ موجود | **پیاده شد در Task قبل** |
**نتیجه:** ✅ تمام Package APIs موجود است
---
## 11. NetworkMembership APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetMyNetworkTree()` | NetworkMembershipService | ✅ موجود | Customer Query جداگانه با ICurrentUserService |
| `GetSubordinateTree()` | NetworkMembershipService | ✅ موجود | Recursive tree traversal |
| `GetMyNetworkStatistics()` | NetworkStatisticsPage.razor | ✅ موجود | با شمارش recursive تمام descendants |
**اصلاحات انجام شده (Feb 5, 2026):**
1.**GetMyNetworkTree Customer Query**:
- ایجاد Query و Handler جداگانه برای Customer
- استفاده از ICurrentUserService به جای UserId در request
- رفع خطای Validation (UserId=0 قبلاً غیرمجاز بود)
2.**GetNetworkStatistics Bug Fix**:
- قبلاً: فقط direct children (depth=1) شمارش می‌شد
- بعد: recursive counting تمام descendants در leftLeg و rightLeg
- متدهای کمکی: `GetAllDescendants()` و `CalculateDepths()`
- فرمول: `leftLegCount = GetAllDescendants(leftChild).Count + 1`
**نتیجه:** ✅ تمام NetworkMembership APIs موجود و اصلاح شده
---
## 12. Commission APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetWeekDefinitions()` | CommissionService | ✅ موجود | **پیاده شد در Task قبل** |
| `GetCommissionBalances()` | CommissionDashboardPage | ✅ موجود | **پیاده شد در Task قبل** |
**نتیجه:** ✅ تمام Commission APIs موجود است
---
## 13. ClubMembership APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `ActivateClubMembership()` | ClubMembershipService | ✅ موجود | Proto موجود در CMS |
| `GetClubMembershipStatus()` | MembershipPage.razor | ✅ موجود | Proto موجود در CMS |
**نتیجه:** ✅ ClubMembership APIs موجود است
---
## 14. Configuration APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetClubConfiguration()` | ClubConfigurationService | ✅ موجود | Proto موجود در CMS |
| `GetClubFeatures()` | FeaturesPage.razor | ✅ موجود | Proto موجود در CMS |
**نتیجه:** ✅ Configuration APIs موجود است
---
## 15. AppVersion APIs
### استفاده شده در FrontOffice
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
| `GetAppVersion()` | AppVersionService | ✅ موجود | Proto موجود در CMS |
**نتیجه:** ✅ AppVersion APIs موجود است
---
## نتیجه‌گیری کلی
### ✅ API های کامل (100% پیاده شده)
1. ✅ User APIs - همه Customer endpoints پیاده شده
2. ✅ Products APIs - GetCustomerProducts و Filter پیاده شده
3. ✅ UserWallet APIs - تمام Customer endpoints پیاده شده
4. ✅ Transaction APIs - Customer endpoints پیاده شده
5. ✅ Package APIs - تمام Customer endpoints پیاده شده
6. ✅ NetworkMembership APIs - پیاده شده
7. ✅ Commission APIs - پیاده شده
8. ✅ Category APIs - GetAllCategoriesForCustomer پیاده شد (Feb 6, 2026)
9. ✅ City APIs - Public API موجود
10. ✅ ClubMembership APIs - Proto موجود
11. ✅ Configuration APIs - Proto موجود
12. ✅ AppVersion APIs - Proto موجود
13.**UserCarts APIs - تمام Customer endpoints پیاده شد (Feb 5, 2026) + اصلاحات Feb 6** 🆕
14.**UserAddress APIs - تمام Customer endpoints پیاده شد (Feb 5, 2026) + باگ Security رفع شد Feb 6** 🆕
15.**UserOrder APIs - Checkout workflow کامل شد (Feb 6, 2026)** 🆕
16.**Products APIs - GetAllProductsByFilter پیاده شد (Feb 6, 2026)** 🆕
### ⚠️ نیاز به توجه
~~1. **UserCarts APIs** - نیاز به Customer-specific endpoints~~
**✅ تکمیل شد - Feb 5, 2026 + اصلاحات Feb 6, 2026**
~~2. **UserAddress APIs** - نیاز به Customer-specific endpoints~~
**✅ تکمیل شد - Feb 5, 2026 + باگ Security رفع شد Feb 6, 2026**
~~3. **UserOrder/Checkout APIs** - نیاز به بررسی~~
**✅ تکمیل شد - Feb 6, 2026:**
- ✅ SubmitShopBuyOrder - تبدیل سبد خرید به سفارش
- ✅ GetUserOrder - نمایش جزئیات سفارش
- ✅ GetAllUserOrderByFilter - لیست سفارشات
- ✅ GetVATRate - دریافت نرخ مالیات 9%
- ✅ OrderDetail.razor - رفع باگ‌های NullReference
4. **UpdateCustomerProfile, ChangeCustomerPassword, UpdateCustomerSettings** - Proto موجود اما Query/Handler نیاز است
---
## اقدامات لازم
~~### Priority 1: UserCarts Customer Endpoints~~
~~این APIs برای سبد خرید ضروری هستند.~~
**✅ تکمیل شد - Feb 5, 2026:**
- ✅ GetCustomerCartQuery و Handler
- ✅ AddToCustomerCartCommand و Handler
- ✅ UpdateCustomerCartItemCommand و Handler
- ✅ RemoveFromCustomerCartCommand و Handler
- ✅ UserCartsService با ISender
**✅ اصلاحات Security - Feb 6, 2026:**
- ✅ CartService.cs: تمام عملیات به Customer APIs تغییر یافت
- ✅ رفع باگ Field Naming: UserCartId → CartItemId
- ✅ رفع منطق حذف: از Update به RemoveUserCartForCustomer
~~### Priority 2: UserAddress Customer Endpoints~~
~~این APIs برای Checkout و مدیریت آدرس‌ها ضروری هستند.~~
**✅ تکمیل شد - Feb 5, 2026:**
- ✅ GetCustomerAddressesQuery و Handler
- ✅ CreateCustomerAddressCommand و Handler
- ✅ UpdateCustomerAddressCommand و Handler
- ✅ DeleteCustomerAddressCommand و Handler
- ✅ SetCustomerDefaultAddressCommand و Handler
- ✅ UserAddressService با ISender
- ⚠️ **یادداشت:** CityName/ProvinceName در response خالی است - FrontOffice باید از City API استفاده کند
**✅ اصلاحات Security - Feb 6, 2026:**
- ✅ Addresses.razor: GetCustomerAddresses (قبلاً تمام آدرس‌ها نمایش می‌یافت)
- ✅ Index.razor (Profile): GetCustomerAddresses
- ✅ CheckoutSummary.razor: GetCustomerAddresses
- ✅ Checkout.razor: GetCustomerAddresses
- ✅ AddAddressDialog.razor: CreateCustomerAddress
- ✅ EditAddressDialog.razor: UpdateCustomerAddress
~~### Priority 3: Checkout/Order Creation~~
باید workflow ثبت سفارش بررسی شود.
### Priority 4: Customer Profile Updates
پیاده‌سازی Handler های Update برای Customer.
---
## وضعیت پروژه
**تکمیل شده:** ~97%
**آخرین به‌روزرسانی:** 6 فوریه 2026
**تغییرات Feb 6, 2026:**
**Phase 1: رفع باگ‌های Critical Security در FrontOffice**
-**UserAddress Security Bug Fix**: تغییر از Admin APIs به Customer APIs در تمام صفحات
- Addresses.razor, Index.razor (Profile), CheckoutSummary.razor, Checkout.razor
- AddAddressDialog, EditAddressDialog
- قبلاً همه آدرس‌های تمام کاربران نمایش داده می‌شد ⚠️
- حالا فقط آدرس‌های کاربر لاگین شده (با ICurrentUserService)
-**UserCart Security Bug Fix**: تغییر از Admin APIs به Customer APIs در CartService
- تمام عملیات: Add, Update, Remove, Clear
- رفع باگ Field Naming: UserCartId → CartItemId (Proto: cart_item_id)
- رفع منطق حذف: از UpdateUserCart با Count=0 به RemoveUserCartForCustomer
- قبلاً تمام سبدهای خرید تمام کاربران قابل دسترسی بود ⚠️
**Phase 2: پیاده‌سازی APIs گم‌شده**
-**GetVATRate**: پیاده‌سازی در UserOrderService
- نرخ مالیات بر ارزش افزوده ایران: 9%
- استفاده در VATService و Products page
-**GetAllProductsByFilter**: پیاده‌سازی در ProductsService
- استفاده از GetCustomerProductsByFilterQuery
- پشتیبانی کامل از filtering, sorting, pagination
- CategoryIds mapping به درستی
-**GetAllCategoriesForCustomer**: پیاده‌سازی در CategoryService
- استفاده از GetAllCategoryByFilterQuery
- فقط دسته‌بندی‌های فعال (IsActive = true)
- ISender به CategoryService اضافه شد
- مرتب‌سازی بر اساس SortOrder
**Phase 3: تکمیل Checkout Workflow**
-**SubmitShopBuyOrder**: تبدیل سبد خرید به سفارش نهایی با **پرداخت از کیف پول** (Updated Feb 6)
- احراز هویت با ICurrentUserService (UserId از JWT)
- اعتبارسنجی سبد خرید (خالی نباشد) و آدرس پیش‌فرض
- محاسبات مالی: مبلغ پایه + مالیات 9% = مبلغ کل
- **اعتبارسنجی موجودی کیف پول**: Balance >= TotalAmount 🆕
- **ایجاد تراکنش**: Type=Buy, PaymentStatus=Success, RefId=SHOP_{timestamp} 🆕
- **کسر از کیف پول**: Balance -= TotalAmount 🆕
- **ثبت لاگ تغییرات**: UserWalletChangeLog با تمام جزئیات (audit trail) 🆕
- ایجاد سفارش (UserOrder): **PaymentStatus=Success, PaymentMethod=Wallet, TransactionId** (Updated from Pending)
- ثبت مالیات (OrderVAT): VATRate, BaseAmount, VATAmount, TotalAmount
- ایجاد جزئیات فاکتور (FactorDetails) برای هر محصول
- پاکسازی سبد خرید (soft delete)
- بازگشت OrderId برای redirect
- **خطاها**: "کیف پول یافت نشد", "موجودی کیف پول کافی نیست"
-**GetUserOrder**: نمایش جزئیات کامل سفارش
- استفاده از GetCustomerOrderQuery
- اطلاعات سفارش + کاربر + آدرس + مالیات + محصولات + ردیابی
- پشتیبانی از nullable fields (PaymentDate, PaymentMethod)
-**GetAllUserOrderByFilter**: لیست سفارشات با فیلتر
- Admin API - می‌تواند همه سفارشات را ببیند
- فیلترها: UserId, PaymentStatus, DeliveryStatus, PaymentDate
- Pagination + Sorting کامل
-**OrderDetail.razor - رفع باگ‌های UI**:
- رفع NullReferenceException برای PaymentDate (null برای سفارشات Pending)
- نمایش "تاریخ ثبت" به جای "تاریخ پرداخت" برای سفارشات بدون پرداخت
- رفع نمایش ProductThumbnailPath به جای ProductTitle
- رفع خطاهای nullable value access: .Value → ?? 0
- محاسبه صحیح subtotal با null coalescing
**خلاصه تغییرات:**
- 🔒 **Security**: رفع باگ‌های critical در UserAddress و UserCart (همه کاربران قابل مشاهده بودند)
- 📦 **Products**: GetAllProductsByFilter + GetAllCategoriesForCustomer پیاده شد
- 💰 **VAT**: GetVATRate با نرخ 9% ایران
- 🛒 **Checkout**: workflow کامل - سبد خرید → سفارش → نمایش جزئیات
- 🐛 **Bug Fixes**: OrderDetail null handling + Field naming (UserCartId → CartItemId)
**تغییرات قبلی (Feb 5, 2026):**
**Phase 1: UserCart & UserAddress Customer Endpoints**
- ✅ پیاده‌سازی کامل UserCart Customer endpoints (4 Handler + Service)
- ✅ پیاده‌سازی کامل UserAddress Customer endpoints (5 Handler + Service)
- ✅ اضافه کردن Proto definitions برای Customer Address
**Phase 2: NetworkMembership Bug Fixes**
- ✅ GetMyNetworkTree Customer Query (رفع خطای Validation)
- ✅ GetNetworkStatistics Recursive Counting (رفع باگ شمارش نادرست)
**Phase 3: UserWallet UI Enhancement**
- ✅ رفع باگ نمایش 0 در مبالغ تراکنش‌ها
- ✅ اضافه کردن CurrentDiscountBalance و ChangeDiscountValue به Proto (v0.0.177)
- ✅ جداسازی تراکنش‌ها به 3 نوع کیف پول (عادی، شبکه، تخفیفی)
- ✅ اصلاح نام‌گذاری: "اعتباری" → "عادی"
- ✅ رفع باگ تاریخ: اضافه کردن ToLocalTime() برای تبدیل UTC
- ✅ UI Desktop: جدول با ستون‌های جداگانه برای هر 3 کیف پول
- ✅ UI Mobile: کارت‌ها با 3 باکس افقی (عادی آبی، شبکه سبز، تخفیفی زرد)
- ✅ نمایش همزمان تغییرات و موجودی مانده برای هر کیف پول
- ✅ تغییر FrontOffice.Main.csproj: PackageReference → ProjectReference
**باقی مانده:**
- ⚠️ Checkout workflow و Order creation (نیاز به بررسی)
- ⚠️ Profile update handlers (UpdateCustomerProfile, ChangePassword, UpdateSettings)
- 📝 CityName/ProvinceName در GetCustomerAddresses خالی است (نیاز به City API lookup در FrontOffice)
**Build Status:**
- ✅ CMS: 0 Errors, ~60 Warnings (unused proto imports)
- ✅ FrontOffice: 0 Errors, ~120 Warnings (nullable references)
**صفحات تست شده (Feb 6):**
- ✅ /profile/addresses - کار می‌کند (فقط آدرس‌های خود کاربر)
- ✅ /products - کار می‌کند (لیست محصولات با filtering و sorting)
- ✅ /categories - کار می‌کند (لیست دسته‌بندی‌های فعال)
- ✅ /profile/wallet - کار می‌کند (3 کیف پول با تراکنش‌های کامل)
-38
View File
@@ -1,38 +0,0 @@
# 🎉 به‌روزرسانی جدید - نسخه ۱.۵.۰
**تاریخ انتشار**: ۹ دی ۱۴۰۴
---
## ✨ امکانات جدید
### 💰 بهبود صفحه پاداش‌ها
- **انتخابگر هفته هوشمند**: حالا می‌تونید با تایپ کردن، هفته مورد نظر رو سریع‌تر پیدا کنید
- **نمایش خلاصه**: در بالای صفحه، مجموع پاداش‌ها، مبلغ پرداخت شده و در انتظار رو ببینید
- **طراحی جدید موبایل**: تجربه بهتر در گوشی موبایل
### 📊 جزئیات بیشتر در گزارش هفتگی
- **نمایش اعضای جدید**: تعداد اعضای جدید هر تیم در هفته
- **انتقال از هفته قبل**: مشاهده امتیازات منتقل شده از هفته گذشته
### 🎨 بهبود رابط کاربری
- طراحی زیباتر کارت‌ها و جداول
- نمایش بهتر در تمام اندازه‌های صفحه نمایش
---
## 🐛 رفع اشکال
- رفع مشکل نمایش نادرست امتیازات منتقل شده
- بهبود سرعت بارگذاری صفحات
---
## 💡 نکته
برای دسترسی به پاداش‌های خود، از منوی **پروفایل** گزینه **پاداش‌های من** را انتخاب کنید.
---
با تشکر از همراهی شما 🙏
**تیم کارا بازار سلامت**
-591
View File
@@ -1,591 +0,0 @@
# پیاده‌سازی ICurrentUserService در سرویس‌های Customer
## خلاصه تغییرات
این سند تمام تغییرات انجام شده برای پیاده‌سازی احراز هویت مبتنی بر JWT در endpoint‌های Customer را مستند می‌کند. هدف اصلی حذف نیاز به ارسال صریح UserId از سمت کلاینت و استخراج خودکار آن از JWT Claims است.
## الگوی پیاده‌سازی
### الگوی Query Handler (با ICurrentUserService)
```csharp
public class SomeQueryHandler : IRequestHandler<SomeQuery, SomeResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser;
public SomeQueryHandler(IApplicationDbContext context, ICurrentUserService currentUser)
{
_context = context;
_currentUser = currentUser;
}
public async Task<SomeResponseDto> Handle(SomeQuery request, CancellationToken cancellationToken)
{
// رزولو کردن UserId از JWT اگر در request مشخص نشده باشد
var userId = request.UserId == 0
? (long.TryParse(_currentUser.UserId, out var currentUserId) ? currentUserId : 0)
: request.UserId;
if (userId == 0)
throw new UnauthorizedAccessException("User ID not found");
var query = _context.SomeEntity
.Where(x => x.UserId == userId)
.AsNoTracking();
// ... ادامه پیاده‌سازی
}
}
```
### الگوی Service (استفاده از ISender)
```csharp
public class SomeService : SomeContract.SomeContractBase
{
private readonly ISender _sender;
public SomeService(ISender sender)
{
_sender = sender;
}
public override async Task<Response> CustomerEndpoint(Request request, ServerCallContext context)
{
var query = new SomeQuery { UserId = 0 }; // 0 = استفاده از ICurrentUserService
var result = await _sender.Send(query, context.CancellationToken);
return MapToProtoResponse(result);
}
}
```
## تصمیمات معماری
### 1. ISender vs IDispatchRequestToCQRS
- **IDispatchRequestToCQRS**: برای endpoint‌های Admin که ساختار Proto به‌طور مستقیم به CQRS نگاشت می‌شود
- **ISender**: برای endpoint‌های Customer که نیاز به ساخت دستی Query و ساختار متفاوت دارند
### 2. قرارداد UserId = 0
- `0` یا مقدار مشخص نشده = استفاده از ICurrentUserService برای دریافت کاربر فعلی از JWT
- مقدار غیر صفر = کاربر صریح (برای عملیات admin/support)
### 3. مسئولیت Query Handler
- Query Handler باید پس از رزولو کردن userId، وجود آن را validate کند
- در صورت عدم موفقیت در تعیین userId، UnauthorizedAccessException پرتاب شود
## سرویس‌های پیاده‌سازی شده
### ✅ 1. UserWallet Service (5 endpoints)
#### 1.1 GetUserWalletQueryHandler
**فایل**: `CMSMicroservice.Application/UserWalletCQ/Queries/GetUserWallet/GetUserWalletQueryHandler.cs`
**تغییرات**:
- افزودن `ICurrentUserService` به constructor
- اضافه شدن فیلد `DiscountBalance` به DTO
- پشتیبانی از `Id = 0` برای استفاده از کاربر فعلی
```csharp
var userId = request.Id == 0
? (long.TryParse(_currentUser.UserId, out var currentUserId) ? currentUserId : 0)
: request.Id;
```
#### 1.2 GetCustomerWalletChangeLogQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/UserWalletCQ/Queries/GetCustomerWalletChangeLog/`
**پیاده‌سازی**:
- Query/Handler جدید برای دریافت تاریخچه تغییرات کیف پول
- استفاده از entity `UserWalletChangeLog`
- پشتیبانی از Pagination
- فیلتر بر اساس userId از ICurrentUserService
#### 1.3 GetCustomerWithdrawalsQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/UserWalletCQ/Queries/GetCustomerWithdrawals/`
**پیاده‌سازی**:
- Query/Handler جدید برای دریافت درخواست‌های برداشت
- استفاده از entity `UserCommissionPayout`
- فیلتر بر اساس `WithdrawalRequestDate` و `status = PayoutRequested`
- پشتیبانی از Pagination
#### 1.4 GetCustomerWithdrawalSettingsQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/UserWalletCQ/Queries/GetCustomerWithdrawalSettings/`
**پیاده‌سازی**:
- Query/Handler جدید برای دریافت تنظیمات برداشت
- مقدار ثابت `MIN_WITHDRAWAL_AMOUNT = 50000`
- برگرداندن موجودی کیف پول کاربر فعلی
#### 1.5 UserWalletService
**فایل**: `CMSMicroservice.WebApi/Services/UserWalletService.cs`
**تغییرات**:
- افزودن `ISender` به constructor
- پیاده‌سازی 4 متد Customer با استفاده از Query Handler‌های واقعی:
- `GetCustomerWallet`
- `GetCustomerWalletChangeLog`
- `GetCustomerWithdrawals`
- `GetCustomerWithdrawalSettings`
---
### ✅ 2. Commission Service (2 endpoints)
#### 2.1 GetUserCommissionPayoutsQueryHandler
**فایل**: `CMSMicroservice.Application/CommissionCQ/Queries/GetUserCommissionPayouts/GetUserCommissionPayoutsQueryHandler.cs`
**تغییرات**:
- افزودن `ICurrentUserService` به constructor
- پشتیبانی از `UserId = null` یا `0` برای استفاده از کاربر فعلی
- کوئری از `UserCommissionPayouts` با Include کردن `WeekDefinition`
#### 2.2 GetUserWeeklyBalancesQueryHandler
**فایل**: `CMSMicroservice.Application/CommissionCQ/Queries/GetUserWeeklyBalances/GetUserWeeklyBalancesQueryHandler.cs`
**تغییرات**:
- افزودن `ICurrentUserService` به constructor
- همان الگوی رزولو UserId
- کوئری از `UserWeeklyBalances` با Include کردن `WeekDefinition`
---
### ✅ 3. NetworkMembership Service (3 endpoints)
#### 3.1 GetNetworkTreeQueryHandler
**فایل**: `CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs`
**تغییرات**:
- افزودن `ICurrentUserService` به constructor
- پشتیبانی از `UserId = 0` برای استفاده از کاربر فعلی
- اجرای Stored Procedure `[CMS].[GetNetworkTree]`
- تبدیل نتایج flat SP به ساختار درختی hierarchical
#### 3.2 GetNetworkStatisticsQueryHandler
**فایل**: `CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkStatistics/GetNetworkStatisticsQueryHandler.cs`
**تغییرات**:
- افزودن پارامتر `UserId` به Query
- افزودن `ICurrentUserService` به constructor
- تغییر منطق از آمار کل سیستم به آمار شبکه زیرمجموعه کاربر
- فیلتر: `x.NetworkParentId == userId` (نه `x.NetworkParentId != null`)
#### 3.3 NetworkMembershipService
**فایل**: `CMSMicroservice.WebApi/Services/NetworkMembershipService.cs`
**تغییرات**:
- افزودن `ISender` به constructor
- پیاده‌سازی 3 متد Customer:
- `GetMyNetworkTree`: درخت شبکه کاربر فعلی با UserId=0
- `GetSubordinateTree`: درخت زیرمجموعه خاص (برای admin)
- `GetMyNetworkStatistics`: آمار شبکه کاربر فعلی
- متدهای helper:
- `ConvertToNodeModel()`: تبدیل بازگشتی DTO به Proto Model
- `CountNodes()`: شمارش بازگشتی node‌های درخت
**رفع باگ**:
- حذف فیلدهای `IsClubActive` و `ActivationWeekDefinitionId` که در Proto request وجود نداشتند
---
### ✅ 4. Package Service (3 query endpoints)
#### 4.1 GetCustomerPackagesQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/PackageCQ/Queries/GetCustomerPackages/`
**پیاده‌سازی**:
- Query/Handler جدید برای دریافت لیست پکیج‌ها
- کوئری از entity `Package`
- نگاشت فیلدهای اضافی:
- `Name = Title`
- `ImageUrl = ImagePath`
- `Currency = "IRR"`
- `ValidityDays = 365`
- پشتیبانی از فیلتر `PackageType` (در صورت وجود در entity)
#### 4.2 GetCustomerPackageDetailsQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/PackageCQ/Queries/GetCustomerPackageDetails/`
**پیاده‌سازی**:
- Query/Handler جدید برای دریافت جزئیات یک پکیج
- کوئری بر اساس `PackageId`
- افزودن Features (کمیسیون، پشتیبانی، آموزش)
- افزودن Requirements (عضویت، موجودی کیف پول، محدودیت‌ها)
#### 4.3 GetCustomerPurchaseHistoryQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/PackageCQ/Queries/GetCustomerPurchaseHistory/`
**پیاده‌سازی**:
- Query/Handler جدید با ICurrentUserService
- کوئری از `UserOrders` با فیلتر `PackageId != null`
- Include کردن navigation property `Package`
- پشتیبانی از:
- Pagination
- فیلتر تاریخ (FromDate, ToDate)
- فیلتر نوع پکیج
- نگاشت `PaymentStatus` صحیح (Success/Reject/Pending)
- دریافت `RefId` از Transaction (نه `ReferenceId`)
#### 4.4 PackageService
**فایل**: `CMSMicroservice.WebApi/Services/PackageService.cs`
**تغییرات**:
- افزودن `ISender` به constructor
- افزودن namespace alias: `using AppModels = CMSMicroservice.Application.Common.Models;`
- جایگزینی 3 متد MOCK با Query Handler واقعی:
- `GetCustomerPackages`
- `GetCustomerPackageDetails`
- `GetCustomerPurchaseHistory`
- رفع ابهام در type‌های `PaginationState` و `MetaData` با استفاده از alias
- متدهای Command (Purchase, Verify) همچنان MOCK باقی ماندند
---
## مشکلات رفع شده
### 1. خطای Type Inference با IDispatchRequestToCQRS
**خطا**: `CS1061: 'Empty' does not contain definition for 'Balance'`
**علت**: استفاده از overload نادرست `Handle<TCommand, TResponse>` که compiler نوع‌ها را اشتباه استنباط می‌کرد
**راه حل**: استفاده از `ISender.Send()` به‌جای `IDispatchRequestToCQRS` برای endpoint‌های Customer
### 2. عدم تطابق فیلدهای Proto
**خطا**: `CS1061: GetSubordinateTreeRequest doesn't have ActivationWeekDefinitionId`
**علت**: کد سرویس فیلدهایی را فرض می‌کرد که در Proto تعریف نشده بودند
**راه حل**: حذف فیلدهای غیرموجود از نگاشت request
### 3. خطای Nullable Protobuf Wrapper
**خطا**: `CS1061: 'long' doesn't contain 'Value' property`
**علت**: تلاش برای فراخوانی `.Value` روی type‌های non-nullable
**راه حل**: حذف فراخوانی `.Value` و انتساب مستقیم
### 4. خطای Transaction.ReferenceId
**خطا**: `CS1061: 'Transaction' does not contain a definition for 'ReferenceId'`
**علت**: نام صحیح فیلد `RefId` است نه `ReferenceId`
**راه حل**: تغییر به `Transaction.RefId`
### 5. خطای PaymentStatus Enum Values
**خطا**: `CS0117: 'PaymentStatus' does not contain a definition for 'Failed'/'Refunded'`
**علت**: enum فقط دارای مقادیر `Success`, `Reject`, `Pending` است
**راه حل**: تصحیح switch statement به مقادیر صحیح
### 6. خطای Ambiguous Reference
**خطا**: `CS0104: 'PaginationState'/'MetaData' is ambiguous`
**علت**: type‌ها هم در `CMSMicroservice.Application.Common.Models` و هم در `CMSMicroservice.Protobuf.Protos` وجود دارند
**راه حل**: افزودن namespace alias: `using AppModels = CMSMicroservice.Application.Common.Models;`
### 7. خطای MetaData Constructor
**خطا**: `CS1729: 'MetaData' does not contain a constructor that takes 3 arguments`
**علت**: MetaData class در Application layer بدون constructor است
**راه حل**: استفاده از object initializer به‌جای constructor:
```csharp
var metaData = new MetaData
{
TotalCount = totalCount,
CurrentPage = pageNumber,
PageSize = pageSize,
TotalPage = (int)Math.Ceiling((double)totalCount / pageSize),
HasPrevious = pageNumber > 1,
HasNext = pageNumber < totalPages
};
```
### 8. خطای CategoryIds در Proto
**خطا**: `CS1061: 'GetAllProductsByFilterFilter' does not contain 'CategoryIds'`
**علت**: Proto فقط `category_id` (singular) دارد نه `category_ids`
**راه حل**: تبدیل single value به List:
```csharp
CategoryIds = request.Filter?.CategoryId != null
? new List<long> { request.Filter.CategoryId.Value }
: null
```
### 9. خطای OrderVAT و DeliveryStatus
**خطا**: `CS1061: 'OrderVAT' does not contain 'VATPercentage'`
**علت**:
- فیلد صحیح `VATRate` است (decimal)
- enum‌های `Processing` و `Shipped` وجود ندارند
**راه حل**:
- استفاده از `VATRate * 100` برای درصد
- تصحیح enum values: `Pending`, `InTransit`, `Delivered`, `Cancelled`, `Returned`
### 10. خطای Transaction/UserWalletChangeLog بدون UserId
**خطا**: `CS1061: 'Transaction/UserWalletChangeLog' does not contain 'UserId'`
**علت**: این entity‌ها direct UserId ندارند
**راه حل**: query از طریق navigation properties:
```csharp
// Transaction
.Include(x => x.UserOrders)
.Where(x => x.UserOrders.Any(o => o.UserId == userId))
// UserWalletChangeLog
.Include(x => x.Wallet)
.Where(x => x.Wallet.UserId == userId)
```
---
### ✅ 5. UserOrder Service (3 endpoints)
#### 5.1 GetCustomerOrdersQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/UserOrderCQ/Queries/GetCustomerOrders/`
**پیاده‌سازی**:
- Query/Handler جدید با ICurrentUserService
- کوئری از `UserOrders` با Include:
- Package, Transaction, UserAddress, User, FactorDetails, OrderVAT
- پشتیبانی از Pagination
- محاسبه `TotalAmount` با احتساب مالیات (`VATRate * 100`)
**رفع باگ**:
- `OrderVAT.VATPercentage` وجود ندارد → استفاده از `VATRate * 100`
- `DeliveryStatus.Processing/Shipped` وجود ندارد → `Pending/InTransit`
#### 5.2 GetCustomerOrderQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/UserOrderCQ/Queries/GetCustomerOrder/`
**پیاده‌سازی**:
- Query/Handler برای دریافت یک سفارش با OrderId
- Validation: بررسی تعلق Order به UserId فعلی
- Include همان navigation properties
#### 5.3 GetCustomerOrderHistoryQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/UserOrderCQ/Queries/GetCustomerOrderHistory/`
**پیاده‌سازی**:
- Query/Handler با Pagination و فیلترها
- فیلترهای پشتیبانی شده:
- FromDate, ToDate
- PaymentStatus, DeliveryStatus
- محاسبه `CanCancelOrder` بر اساس شرایط:
- PaymentStatus = Pending
- DeliveryStatus = None یا Pending
#### 5.4 UserOrderService
**فایل**: `CMSMicroservice.WebApi/Services/UserOrderService.cs`
**تغییرات**:
- افزودن ISender به constructor
- پیاده‌سازی 3 متد Customer با Query Handler واقعی
- استفاده از namespace alias برای حل ambiguity
---
### ✅ 6. Transaction Service (2 endpoints)
#### 6.1 GetCustomerTransactionQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/TransactionsCQ/Queries/GetCustomerTransaction/`
**پیاده‌سازی**:
- Query/Handler با ICurrentUserService
- **چالش**: Transaction entity بدون UserId
- **راه حل**: query از طریق `UserOrders` navigation:
```csharp
.Include(x => x.UserOrders)
.Where(x => x.UserOrders.Any(o => o.UserId == userId))
```
- فیلتر بر اساس Id یا Authority
#### 6.2 GetCustomerTransactionsByFilterQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/TransactionsCQ/Queries/GetCustomerTransactionsByFilter/`
**پیاده‌سازی**:
- Query/Handler با Pagination
- فیلترهای پشتیبانی شده:
- Id, Amount, Description
- PaymentStatus (bool), RefId, Type
- همان الگوی query از طریق UserOrders
#### 6.3 TransactionsService
**فایل**: `CMSMicroservice.WebApi/Services/TransactionsService.cs`
**تغییرات**:
- افزودن ISender و Query imports
- جایگزینی MOCK با Query Handler واقعی
- mapping صحیح Proto enums
---
### ✅ 7. Products Service (2 endpoints)
#### 7.1 GetCustomerProductsQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/ProductsCQ/Queries/GetCustomerProducts/`
**پیاده‌سازی**:
- Query/Handler بدون ICurrentUserService (محصولات عمومی)
- کوئری از `Products` با Include:
- ProductGalleries.ProductImage
- ProductCategories.Category
- ساخت درختی Category Path با متد `BuildCategoryPath()`
- بازگشت بازگشتی به parent categories
#### 7.2 GetCustomerProductsByFilterQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/ProductsCQ/Queries/GetCustomerProductsByFilter/`
**پیاده‌سازی**:
- Query/Handler با Pagination
- فیلترهای کامل:
- Id, Title, Description, ShortInfomation, FullInformation
- Price, Discount, Rate
- SaleCount, ViewCount, RemainingCount
- CategoryIds (لیست شناسه دسته‌بندی‌ها)
- Sorting پویا با `ApplyOrder()`
#### 7.3 ProductsService
**فایل**: `CMSMicroservice.WebApi/Services/ProductsService.cs`
**تغییرات**:
- افزودن ISender به constructor
- پیاده‌سازی 2 متد Customer
- mapping دستی Gallery و Categories به Proto structures
- **رفع باگ**: Proto فقط `category_id` دارد نه `category_ids`
- تبدیل single value به List<long>
---
### ✅ 8. User Service (3 endpoints)
#### 8.1 GetCustomerProfileQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/UserCQ/Queries/GetCustomerProfile/`
**پیاده‌سازی**:
- Query/Handler با ICurrentUserService
- دریافت پروفایل کامل کاربر فعلی
- محاسبه `ProfileCompletionPercentage` بر اساس 10 فیلد:
- FirstName, LastName, Mobile, Email, NationalCode
- AvatarPath, BirthDate, IsMobileVerified
- NetworkParentId, ReferralCode
- محاسبه `FullName` از FirstName + LastName
#### 8.2 GetCustomerReferralsQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/UserCQ/Queries/GetCustomerReferrals/`
**پیاده‌سازی**:
- Query/Handler با ICurrentUserService و Pagination
- کوئری کاربران با `NetworkParentId == userId`
- فیلتر بر اساس StatusFilter (ACTIVE/INACTIVE/ALL)
- محاسبه آمار:
- TotalReferrals, ActiveReferrals
- TotalCommissionEarned از `UserWallet.NetworkBalance`
- ThisMonthCommission از `UserWalletChangeLog`
- **رفع باگ**: UserWalletChangeLog بدون UserId
- راه حل: `.Include(x => x.Wallet).Where(x => x.Wallet.UserId == userId)`
#### 8.3 GetCustomerSettingsQueryHandler (جدید)
**فایل**: `CMSMicroservice.Application/UserCQ/Queries/GetCustomerSettings/`
**پیاده‌سازی**:
- Query/Handler ساده برای دریافت تنظیمات کاربر
- فیلدهای موجود در User entity:
- EmailNotifications, SmsNotifications, PushNotifications
- مقادیر پیش‌فرض برای فیلدهای ناموجود:
- MarketingNotifications = false
- PreferredLanguage = "fa"
- TimeZone = "Asia/Tehran"
- TwoFactorAuthEnabled = false
#### 8.4 UserService
**فایل**: `CMSMicroservice.WebApi/Services/UserService.cs`
**تغییرات**:
- افزودن ISender و Query imports
- پیاده‌سازی 3 متد Customer با Query Handler واقعی
- تبدیل DateTime به Timestamp با `SpecifyKind(DateTimeKind.Utc)`
- **رفع ambiguity**: fully qualified names برای CustomerReferralStats و CustomerReferralModel
---
## آمار پیشرفت
### سرویس‌های تکمیل شده (8/8): ✅ 100%
✅ **UserWallet** (5 endpoints)
✅ **Commission** (2 endpoints)
✅ **NetworkMembership** (3 endpoints)
✅ **Package** (3 endpoints)
✅ **UserOrder** (3 endpoints)
✅ **Transaction** (2 endpoints)
✅ **Products** (2 endpoints)
✅ **User** (3 endpoints)
**جمع کل**: **25 endpoint** با الگوی ICurrentUserService پیاده‌سازی شد
---
## نکات فنی
### Entity Navigation Properties
همیشه از `.Include()` برای load کردن navigation property‌های مورد نیاز استفاده شود:
```csharp
query = query.Include(x => x.Package)
.Include(x => x.Transaction);
```
### Pagination
از extension method‌های `GetMetaData` و `PaginatedListAsync` استفاده شود:
```csharp
var metaData = await query.GetMetaData(request.PaginationState, cancellationToken);
var items = await query.PaginatedListAsync(request.PaginationState).ToListAsync(cancellationToken);
```
### DateTime Mapping
برای تبدیل به Protobuf Timestamp، DateTime باید UTC باشد:
```csharp
Timestamp.FromDateTime(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc))
```
### Enum Casting
برای نگاشت enum‌ها بین Application و Proto:
```csharp
Status = (PaymentStatusEnum)order.PaymentStatus
```
---
## Build Status
**آخرین Build موفق**: 0 Error(s), 66 Warning(s) - Time Elapsed 00:00:03.55
---
## تاریخ آخرین به‌روزرسانی
5 فوریه 2026
---
## نتیجه‌گیری
پیاده‌سازی ICurrentUserService در **25 endpoint** مربوط به **8 سرویس** با موفقیت کامل شد.
### دستاوردها:
-**100% Coverage**: تمام endpoint‌های Customer پیاده‌سازی شدند
-**الگوی Consistent**: pattern مشخص برای تمام سرویس‌ها
-**امنیت بالا**: استخراج خودکار UserId از JWT
-**قابلیت نگهداری**: کد تمیز و قابل فهم
-**Build موفق**: بدون هیچ خطا
### چالش‌های حل شده:
- Entity‌های بدون UserId (Transaction, UserWalletChangeLog)
- Proto/Application type ambiguity
- MetaData بدون constructor
- Category path building
- Proto enum mapping
- DateTime UTC conversion
تمام تغییرات compile می‌شوند و آماده تست و deployment هستند.
-144
View File
@@ -1,144 +0,0 @@
# بهبود سیستم موجودی (Inventory System Improvements)
> **تاریخ:** اسفند ۱۴۰۴ (February 2026)
> **وضعیت:** ✅ پیاده‌سازی شده — مرج به production
> **پروژه‌های تغییر یافته:** CMS, BackOffice
---
## ۱. خلاصه تغییرات
| تغییر | پروژه | وضعیت |
|-------|--------|--------|
| ایجاد خودکار رکورد موجودی هنگام ساخت محصول عادی | CMS | ✅ |
| سرویس مهاجرت یکبار‌اجرا برای محصولات بدون رکورد موجودی | CMS | ✅ |
| اتوکامپلیت محصولات تخفیفی | BackOffice | ✅ |
| UX هوشمند دیالوگ ورود کالا | BackOffice | ✅ |
---
## ۲. CMS — ایجاد خودکار رکورد موجودی
### ۲.۱ مشکل
وقتی محصول جدید عادی ساخته می‌شد، رکورد `InventoryItem` ایجاد نمی‌شد. این باعث می‌شد محصول در بخش موجودی نمایش داده نشود تا ادمین دستی آن را اضافه کند.
> **نکته:** `CreateDiscountProductCommandHandler` از قبل `IInventoryService.InitializeInventoryAsync` را فراخوانی می‌کرد — فقط handler محصول عادی این قابلیت را نداشت.
### ۲.۲ تغییر در `CreateNewProductsCommandHandler`
**فایل:** `CMS/src/CMSMicroservice.Application/ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommandHandler.cs`
```csharp
// تزریق IInventoryService
private readonly IInventoryService _inventoryService;
// بعد از SaveChangesAsync:
try
{
await _inventoryService.InitializeInventoryAsync(entity.Id, ProductType.RegularProduct, 0);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to auto-initialize inventory for product {ProductId}", entity.Id);
}
```
- موجودی با `qty=0` ایجاد می‌شود
- خطای موجودی باعث شکست ایجاد محصول نمی‌شود (try/catch)
---
## ۳. CMS — InventoryInitializerService (مهاجرت یکبار‌اجرا)
### ۳.۱ هدف
محصولات قدیمی (legacy) که قبل از اضافه شدن منطق خودکار ساخته شده بودند، رکورد `InventoryItem` ندارند. این سرویس هنگام استارت اپلیکیشن اجرا شده و برای آن‌ها رکورد ایجاد می‌کند.
### ۳.۲ پیاده‌سازی
**فایل:** `CMS/src/CMSMicroservice.Infrastructure/BackgroundServices/InventoryInitializerService.cs`
```
BackgroundService (one-time execution on startup)
├── پیدا کردن Products بدون InventoryItem
├── پیدا کردن DiscountProducts بدون InventoryItem
├── ایجاد InventoryItem برای هر کدام (qty = RemainingCount)
└── ذخیره و توقف
```
**ثبت در DI:**
```csharp
// ConfigureServices.cs
services.AddHostedService<InventoryInitializerService>();
```
### ۳.۳ ویژگی‌ها
- **یکبار اجرا:** بعد از اتمام، سرویس متوقف می‌شود
- **موجودی اولیه:** از `RemainingCount` محصول (نه صفر) برای رکوردهای legacy
- **لاگ‌گیری:** تعداد محصولات بدون رکورد و نتیجه عملیات لاگ می‌شود
- **مقاوم در برابر خطا:** خطا باعث شکست اپلیکیشن نمی‌شود
---
## ۴. BackOffice — DiscountProductsAutoComplete
### ۴.۱ هدف
کامپوننت autocomplete برای جستجوی محصولات تخفیفی (مشابه `ProductsAutoComplete` موجود).
### ۴.۲ فایل‌ها
| فایل | توضیح |
|------|-------|
| `Pages/AutoComplete/DiscountProductsAutoComplete.razor` | UI کامپوننت |
| `Pages/AutoComplete/DiscountProductsAutoComplete.razor.cs` | Code-behind |
### ۴.۳ ویژگی‌ها
- استفاده از `DiscountProductContract.DiscountProductContractClient` (gRPC)
- دِبانس ۷۰۰ms
- حداکثر ۹ نتیجه
- پارامتر خروجی `SelectedProductId` (EventCallback)
- `SearchQuery` از نوع `string` (نه `StringValue`)
---
## ۵. BackOffice — UX هوشمند AddStockDialog
### ۵.۱ مشکل قبلی
دیالوگ «ورود کالا» یک فیلد عددی خام برای وارد کردن شناسه محصول داشت. ادمین باید شناسه را حفظ بوده یا از جایی کپی می‌کرد.
### ۵.۲ تغییر
**فایل:** `BackOffice/src/BackOffice/Pages/Inventory/Components/AddStockDialog.razor`
```
وقتی ProductIdParam == null (دکمه «ورود کالا» از نوار ابزار):
├── انتخاب نوع محصول (فروشگاه عادی / فروشگاه اعتباری)
├── if فروشگاه عادی → نمایش ProductsAutoComplete
├── if فروشگاه اعتباری → نمایش DiscountProductsAutoComplete
└── ولیدیشن: محصول باید انتخاب شده باشد
وقتی ProductIdParam != null (از صفحه محصول):
└── فقط فیلد تعداد نمایش داده می‌شود
```
### ۵.۳ UX هوشمند
- انتخاب «فروشگاه عادی» → فقط محصولات عادی در autocomplete
- انتخاب «فروشگاه اعتباری» → فقط محصولات تخفیفی در autocomplete
- جلوگیری از اشتباه ادمین
---
## ۶. خلاصه فایل‌های تغییر یافته
### CMS
| فایل | نوع تغییر |
|------|-----------|
| `ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommandHandler.cs` | ✏️ ویرایش |
| `BackgroundServices/InventoryInitializerService.cs` | 🆕 جدید |
| `Infrastructure/ConfigureServices.cs` | ✏️ ویرایش (ثبت سرویس) |
### BackOffice
| فایل | نوع تغییر |
|------|-----------|
| `Pages/AutoComplete/DiscountProductsAutoComplete.razor` | 🆕 جدید |
| `Pages/AutoComplete/DiscountProductsAutoComplete.razor.cs` | 🆕 جدید |
| `Pages/Inventory/Components/AddStockDialog.razor` | ✏️ ویرایش |
-190
View File
@@ -1,190 +0,0 @@
# وضعیت Refactoring سیستم انبارداری (Inventory)
**تاریخ:** ۳ ژانویه ۲۰۲۶
**وضعیت:** ✅ تکمیل شده - Build موفق
---
## 📊 وضعیت Build
| پروژه | وضعیت |
|-------|--------|
| CMSMicroservice.Domain | ✅ OK |
| CMSMicroservice.Application | ✅ OK |
| CMSMicroservice.Infrastructure | ✅ OK |
| CMSMicroservice.WebApi | ✅ OK |
---
## ✅ کارهای انجام شده
### 1. حذف Repository Pattern
فایل‌های حذف شده:
- `Application/Common/Interfaces/Repositories/IInventoryItemRepository.cs`
- `Application/Common/Interfaces/Repositories/IStockMovementRepository.cs`
- `Application/Common/Interfaces/Repositories/IWarehouseRepository.cs`
- `Infrastructure/Persistence/Repositories/InventoryItemRepository.cs`
- `Infrastructure/Persistence/Repositories/StockMovementRepository.cs`
- `Infrastructure/Persistence/Repositories/WarehouseRepository.cs`
### 2. حذف Features قدیمی
فولدر حذف شده:
- `Application/Features/` (کل فولدر)
### 3. ایجاد ساختار CQ جدید
#### WarehouseCQ/
```
WarehouseCQ/
├── Commands/
│ ├── CreateWarehouse/
│ ├── UpdateWarehouse/
│ ├── DeleteWarehouse/
│ └── SetDefaultWarehouse/
└── Queries/
├── GetWarehouse/
├── GetAllWarehouses/
└── SearchWarehouses/
```
#### InventoryItemCQ/
```
InventoryItemCQ/
├── Commands/
│ ├── CreateInventoryItem/
│ ├── UpdateInventoryItem/
│ ├── DeleteInventoryItem/
│ ├── UpdateInventoryQuantity/
│ ├── ReserveInventory/
│ ├── ReleaseReservedInventory/
│ ├── ReduceInventory/
│ └── IncreaseInventory/
└── Queries/
├── GetInventoryItem/
├── GetInventoryByProduct/
├── GetAllInventoryItems/
└── GetLowStockItems/
```
#### StockMovementCQ/
```
StockMovementCQ/
├── Commands/
│ └── CreateStockMovement/
└── Queries/
├── GetStockMovements/
└── GetStockMovementsByInventoryItem/
```
### 4. Fix شدن InventoryProfile.cs
- اصلاح enum names: `ProtoProductType.Unspecified` بجای `ProductTypeUnspecified`
- حذف `new Int64Value` - Proto مستقیم `long?` میگیره
- اصلاح expression tree برای `?.` operator
### 5. ساده‌سازی InventoryService.cs
- متدهای اصلی (Warehouse, Query ها) کامل پیاده‌سازی شدن
- متدهای پیچیده که نیاز به lookup دارن فعلاً TODO هستن
---
## ⚠️ متدهای TODO در InventoryService
این متدها نیاز به پیاده‌سازی دارن (وقتی لازم شد):
| متد | دلیل TODO |
|-----|-----------|
| `AddStock` | نیاز به lookup با ProductId/ProductType |
| `AdjustStock` | نیاز به lookup با ProductId/ProductType |
| `ReserveStock` | نیاز به lookup با ProductId/ProductType |
| `ReleaseReservation` | نیاز به lookup با ProductId/ProductType |
| `ConfirmSale` | نیاز به lookup با ProductId/ProductType |
| `ProcessReturn` | نیاز به lookup با ProductId/ProductType |
| `RecordLoss` | نیاز به lookup با ProductId/ProductType |
| `BulkAddStock` | نیاز به loop و lookup |
| `BulkAdjustStock` | نیاز به loop و lookup |
| `GetInventorySummary` | نیاز به Query جدید |
| `GetStockValueReport` | نیاز به Query جدید |
---
## 🎯 درس‌های آموخته شده
1. **همیشه اول Proto رو بررسی کن** - Proto مرجع اصلی API هست
2. **ساختار موجود رو تحلیل کن** - قبل از ساختن فایل جدید، نمونه‌های موجود رو ببین
3. **Mapping از Proto به Command** - نه برعکس!
4. **IApplicationDbContext** - الگوی استاندارد این پروژه برای دسترسی به DB
5. **بدون Repository** - این پروژه از Repository pattern استفاده نمیکنه
6. **Proto enum names** - نام‌ها در C# متفاوت هستن (مثلاً `Unspecified` بجای `PRODUCT_TYPE_UNSPECIFIED`)
7. **Int64Value در Proto** - در C# به `long?` تبدیل میشه، نیازی به `new Int64Value` نیست
---
## 🔄 همگام‌سازی BFF با CMS (۳ ژانویه ۲۰۲۶)
### تغییرات Proto
BackOffice.BFF.Inventory.Protobuf با CMS همگام شد:
| آیتم | قبل | بعد |
|------|-----|-----|
| ProductType enum | `REGULAR`, `DISCOUNT` | `REGULAR_PRODUCT`, `DISCOUNT_PRODUCT` |
| StockMovementType | Sequential (0-9) | Grouped (10, 20, 30, 40, 50) |
| Pagination | `page_index` | `page` |
| Search | `search_term` | `search` |
| Product name | `product_name` | `product_title` |
### فایل‌های آپدیت شده در BFF
**Commands:**
- `AddStock` - حذف Success, Message از Response
- `AdjustStock` - Note→Reason, +ReferenceNumber
- `RecordLoss` - Note→Reason, +ReferenceNumber
- `UpdateInventorySettings` - InventoryItemId→Id
**Queries:**
- `GetAllInventoryItems` - PageIndex→Page, SearchTerm→Search, +ProductPrice
- `GetStockMovements` - PageIndex→Page, +ProductTitle, +Created
- `GetLowStockItems` - حذف Count، استفاده از Page/PageSize
- `GetAllWarehouses` - ActiveOnly→IsActive, +Created, +LastModified
**Mappings:**
- `InventoryProfile.cs` - بازنویسی کامل برای فیلدهای جدید
### وضعیت Build BFF
```
Build succeeded.
0 Warning(s)
0 Error(s)
```
---
## 📊 پوشش API - مقایسه CMS و BFF
| عملیات | CMS | BFF | یادداشت |
|--------|-----|-----|---------|
| GetAllInventoryItems | ✅ | ✅ | همگام |
| GetInventoryItem | ✅ | ✅ | همگام |
| GetLowStockItems | ✅ | ✅ | همگام |
| GetStockMovements | ✅ | ✅ | همگام |
| GetAllWarehouses | ✅ | ✅ | همگام |
| AddStock | ✅ | ✅ | همگام |
| AdjustStock | ✅ | ✅ | همگام |
| RecordLoss | ✅ | ✅ | همگام |
| CreateWarehouse | ✅ | ✅ | همگام |
| UpdateWarehouse | ✅ | ❌ | نیاز به پیاده‌سازی |
| UpdateInventorySettings | ✅ | ✅ | همگام |
| GetInventorySummary | TODO | ❌ | اولویت بالا |
| GetStockValueReport | TODO | ❌ | اولویت بالا |
| ProcessReturn | TODO | ❌ | اولویت متوسط |
---
## 📝 نتیجه‌گیری
**Refactoring با موفقیت تکمیل شد!**
- Application layer با ساختار `*CQ/Commands/[Action]/` سازگار شد
- Repository pattern کاملاً حذف شد
- WebApi layer با Proto سازگار شد
- Build همه پروژه‌ها موفق هست
- **BFF کاملاً با CMS همگام شد (۳ ژانویه ۲۰۲۶)**
-303
View File
@@ -1,303 +0,0 @@
# 📦 Product Bundle Feature (پکیج محصولات)
> **وضعیت:** ⏸️ Postponed - مستند شده برای پیاده‌سازی آینده
>
> **تاریخ:** ۱۲ دی ۱۴۰۴ (1 January 2026)
---
## 📋 خلاصه نیازمندی
امکان ایجاد **پکیج محصولات** که:
- یک محصول با نوع "پکیج" ایجاد می‌شود (همه فیلدها مثل محصول عادی)
- این پکیج شامل **چند محصول** است
- هنگام **خرید پکیج**، موجودی **تمام محصولات داخل** کم می‌شود
- هنگام **مرجوعی**، موجودی تمام محصولات برمی‌گردد
---
## 🏗️ تغییرات مورد نیاز
### 1. Domain Layer
#### 1.1 Enum جدید: `ProductTypeCategory`
```csharp
// CMSMicroservice.Domain/Enums/ProductTypeCategory.cs
public enum ProductTypeCategory
{
Simple = 1, // محصول ساده
Bundle = 2 // پکیج (بسته محصولات)
}
```
#### 1.2 فیلد جدید در `Product` Entity
```csharp
// Product.cs - اضافه کردن فیلد
public ProductTypeCategory TypeCategory { get; set; } = ProductTypeCategory.Simple;
```
#### 1.3 Entity جدید: `ProductBundleItem` (جدول واسط)
```csharp
// CMSMicroservice.Domain/Entities/ProductBundleItem.cs
public class ProductBundleItem : BaseAuditableEntity
{
/// <summary>
/// شناسه محصول پکیج (والد)
/// </summary>
public long BundleProductId { get; set; }
public virtual Product BundleProduct { get; set; } = null!;
/// <summary>
/// شناسه محصول داخل پکیج (فرزند)
/// </summary>
public long ChildProductId { get; set; }
public virtual Product ChildProduct { get; set; } = null!;
/// <summary>
/// تعداد این محصول در پکیج
/// </summary>
public int Quantity { get; set; } = 1;
}
```
### 2. Infrastructure Layer
#### 2.1 DbContext Configuration
```csharp
// ApplicationDbContext.cs
public DbSet<ProductBundleItem> ProductBundleItems => Set<ProductBundleItem>();
// Configuration
modelBuilder.Entity<ProductBundleItem>(entity =>
{
entity.ToTable("ProductBundleItems", "CMS");
entity.HasOne(x => x.BundleProduct)
.WithMany(p => p.BundleItems)
.HasForeignKey(x => x.BundleProductId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(x => x.ChildProduct)
.WithMany()
.HasForeignKey(x => x.ChildProductId)
.OnDelete(DeleteBehavior.Restrict);
// یک محصول فقط یکبار در یک پکیج
entity.HasIndex(x => new { x.BundleProductId, x.ChildProductId }).IsUnique();
});
```
#### 2.2 آپدیت `InventoryService.ConfirmSaleAsync()`
```csharp
public async Task<bool> ConfirmSaleAsync(
long productId,
ProductType productType,
int quantity,
long? orderId = null,
CancellationToken ct = default)
{
// چک کردن آیا محصول پکیج است
var product = await _dbContext.Products
.Include(p => p.BundleItems)
.ThenInclude(bi => bi.ChildProduct)
.FirstOrDefaultAsync(p => p.Id == productId, ct);
if (product?.TypeCategory == ProductTypeCategory.Bundle)
{
// کم کردن موجودی تمام محصولات داخل پکیج
foreach (var bundleItem in product.BundleItems)
{
await ConfirmSaleForSingleProduct(
bundleItem.ChildProductId,
productType,
quantity * bundleItem.Quantity, // ضرب در تعداد خرید شده
orderId,
ct);
}
return true;
}
// محصول ساده - روال عادی
return await ConfirmSaleForSingleProduct(productId, productType, quantity, orderId, ct);
}
```
### 3. Application Layer
#### 3.1 آپدیت `CreateNewProductsCommand`
```csharp
public record CreateNewProductsCommand : IRequest<long>
{
// ... existing fields ...
public ProductTypeCategory TypeCategory { get; init; } = ProductTypeCategory.Simple;
/// <summary>
/// لیست محصولات داخل پکیج (فقط وقتی TypeCategory == Bundle)
/// </summary>
public List<BundleItemDto>? BundleItems { get; init; }
}
public record BundleItemDto
{
public long ProductId { get; init; }
public int Quantity { get; init; } = 1;
}
```
#### 3.2 Repository جدید: `IProductBundleItemRepository`
```csharp
public interface IProductBundleItemRepository : IRepository<ProductBundleItem>
{
Task<List<ProductBundleItem>> GetByBundleProductIdAsync(long bundleProductId, CancellationToken ct = default);
Task SetBundleItemsAsync(long bundleProductId, List<(long ProductId, int Quantity)> items, CancellationToken ct = default);
}
```
### 4. Proto/gRPC Layer
#### 4.1 آپدیت `products.proto`
```protobuf
enum ProductTypeCategory {
PRODUCT_TYPE_SIMPLE = 0;
PRODUCT_TYPE_BUNDLE = 1;
}
message BundleItemMessage {
int64 product_id = 1;
int32 quantity = 2;
}
message CreateNewProductsRequest {
// ... existing fields ...
ProductTypeCategory type_category = 15;
repeated BundleItemMessage bundle_items = 16;
}
message ProductDto {
// ... existing fields ...
ProductTypeCategory type_category = 20;
repeated BundleItemMessage bundle_items = 21;
}
```
---
## 📊 دیاگرام رابطه‌ها
```
┌─────────────────┐
│ Products │
├─────────────────┤
│ Id │◄──────────────────┐
│ Title │ │
│ TypeCategory │ ← Simple/Bundle │
│ ... │ │
└────────┬────────┘ │
│ │
│ 1:N (Bundle → Items) │
▼ │
┌─────────────────────┐ │
│ ProductBundleItems │ │
├─────────────────────┤ │
│ Id │ │
│ BundleProductId (FK)│───────────────┘
│ ChildProductId (FK) │───────────────┐
│ Quantity │ │
└─────────────────────┘ │
┌────────────────────────────┘
┌─────────────────┐
│ Products │
│ (Child Item) │
└─────────────────┘
```
---
## 🔄 Flow خرید پکیج
```
1. کاربر پکیج را به سبد اضافه می‌کند
└── CartItem { ProductId: 100, Count: 2 } // پکیج شامل 3 محصول
2. سفارش ثبت می‌شود
└── PlaceOrderCommandHandler.ReserveStock()
├── Check: Product.TypeCategory == Bundle
├── Get: BundleItems = [
│ { ChildProductId: 10, Quantity: 1 },
│ { ChildProductId: 20, Quantity: 2 },
│ { ChildProductId: 30, Quantity: 1 }
│ ]
└── Reserve:
├── Product 10: Reserve 2×1 = 2 عدد
├── Product 20: Reserve 2×2 = 4 عدد
└── Product 30: Reserve 2×1 = 2 عدد
3. پرداخت موفق
└── ConfirmSaleAsync()
├── Product 10: -2 از موجودی
├── Product 20: -4 از موجودی
└── Product 30: -2 از موجودی
4. مرجوعی (در صورت نیاز)
└── ProcessReturnAsync()
├── Product 10: +2 به موجودی
├── Product 20: +4 به موجودی
└── Product 30: +2 به موجودی
```
---
## ⚠️ محدودیت‌ها و قوانین
1. **محصول پکیج خودش موجودی ندارد** - فقط موجودی محصولات داخلش مهم است
2. **پکیج داخل پکیج ممنوع** - فقط محصولات ساده (`Simple`) می‌توانند داخل پکیج باشند
3. **حذف محصول از پکیج** - اگر محصولی در پکیج استفاده شده، نمی‌تواند حذف شود
4. **موجودی قابل فروش پکیج** = `MIN(موجودی هر محصول داخل / تعداد آن در پکیج)`
---
## 📁 فایل‌های جدید/تغییریافته
### فایل‌های جدید:
- `CMSMicroservice.Domain/Enums/ProductTypeCategory.cs`
- `CMSMicroservice.Domain/Entities/ProductBundleItem.cs`
- `CMSMicroservice.Application/Features/ProductBundleItems/*`
- `CMSMicroservice.Infrastructure/Repositories/ProductBundleItemRepository.cs`
### فایل‌های تغییریافته:
- `CMSMicroservice.Domain/Entities/Product.cs` - اضافه کردن `TypeCategory` و `BundleItems`
- `CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs` - DbSet و Configuration
- `CMSMicroservice.Infrastructure/Services/InventoryService.cs` - منطق پکیج
- `CMSMicroservice.Application/ProductsCQ/Commands/CreateNewProducts/*`
- `CMSMicroservice.Protobuf/Protos/products.proto`
- Order Handlers (Reserve, Confirm, Release)
---
## ⏱️ تخمین زمان
| تسک | زمان تخمینی |
|-----|-------------|
| Domain entities & enums | 30 دقیقه |
| EF Migration | 15 دقیقه |
| Repository | 30 دقیقه |
| InventoryService update | 1 ساعت |
| CQRS handlers | 1 ساعت |
| Proto & gRPC | 45 دقیقه |
| تست و دیباگ | 1 ساعت |
| **جمع** | **~5 ساعت** |
---
## 📝 یادداشت‌ها
- این فیچر با پکیج عضویت (`Package` entity موجود) متفاوت است
- نیاز به تست دقیق منطق انبارداری دارد
- UI نیاز به multi-select برای انتخاب محصولات داخل پکیج دارد
---
*این داکیومنت برای پیاده‌سازی آینده نگهداری می‌شود.*
-187
View File
@@ -1,187 +0,0 @@
# 🔐 فیکس فلوی ثبت‌نام / ورود FrontOffice
**تاریخ:** بهمن ۱۴۰۴ (February 2026)
---
## 📋 خلاصه
بررسی کامل فلوی ثبت‌نام و ورود FrontOffice از UI تا دیتابیس انجام شد. **۳ باگ بحرانی** شناسایی و رفع شده:
| # | شدت | مشکل | فایل |
|---|------|-------|------|
| 1 | 🔴 بحرانی | کاربران جدید ثبت‌نام نمی‌شوند | `UserCQ/VerifyOtpToken/VerifyOtpTokenCommandHandler.cs` |
| 2 | 🔴 بحرانی | امضای قرارداد همیشه شکست می‌خورد | `UserCQ/AcceptContract/AcceptContractCommandHandler.cs` |
| 3 | 🟡 متوسط | منوی کناری وضعیت نادرست نشان می‌دهد | `FrontOffice.Main/Utilities/AuthService.cs` |
---
## 🔴 باگ ۱ — کاربران جدید ثبت‌نام نمی‌شوند
### مشکل
FrontOffice از `UserContract.UserContractClient` (user.proto) استفاده می‌کند → `UserCQ/VerifyOtpTokenCommandHandler`. این handler وقتی کاربر یافت نمی‌شد فقط خطای **«کاربر یافت نشد»** برمی‌گرداند و کاربر جدید ایجاد **نمی‌کرد**.
لاجیک ایجاد کاربر (شامل: اعتبارسنجی کد معرف، درخت باینری، موقعیت شاخه) در `OtpTokenCQ/VerifyOtpTokenCommandHandler` بود — سرویسی که FrontOffice اصلاً از آن استفاده نمی‌کند.
### رفع
اضافه شدن لاجیک کامل ایجاد کاربر جدید به `UserCQ/VerifyOtpTokenCommandHandler`:
```
if (user == null)
{
// ۱. اعتبارسنجی کد معرف (ParentReferralCode) — الزامی
// ۲. بررسی وجود معرف و فعال بودن عضویت باشگاه
// ۳. بررسی ظرفیت (حداکثر ۲ زیرمجموعه مستقیم)
// ۴. تعیین شاخه (چپ اول، بعد راست)
// ۵. ایجاد User + UserRole + UserWallet
// ۶. رویدادهای دامنه: CreateNewUserEvent, CreateNewUserRoleEvent, CreateNewUserWalletEvent
// ۷. بارگذاری مجدد کاربر با روابط کامل
// ۸. تولید JWT token
}
```
### اعتبارسنجی‌ها
| مرحله | شرط | پیام خطا |
|-------|------|---------|
| کد معرف | خالی یا null | «کد معرف الزامی است» |
| معرف | وجود نداشته باشد | «معرف وجود ندارد» |
| عضویت باشگاه | غیرفعال باشد | «لینک دعوت معرف فعال نیست» |
| ظرفیت | بیش از ۱ فرزند | «ظرفیت معرف تکمیل است» |
| شاخه | هر دو پُر باشند | «ظرفیت معرف تکمیل است» |
### فایل
`CMS/src/CMSMicroservice.Application/UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommandHandler.cs`
---
## 🔴 باگ ۲ — امضای قرارداد همیشه شکست می‌خورد
### مشکل
`AcceptContractCommandHandler` از `_currentUserService.Username` برای پیدا کردن OTP و کاربر بر اساس شماره موبایل استفاده می‌کرد:
```csharp
// ❌ قبل — Username = "{FirstName} {LastName}" نه شماره موبایل!
var otpToken = await _context.OtpTokens
.Where(x => x.Mobile == _currentUserService.Username ...)
var user = await _context.Users
.Where(x => x.Mobile == _currentUserService.Username ...)
```
**`CurrentUserService.Username`** مقدار `ClaimTypes.Name` را برمی‌گرداند که در JWT به صورت `"{FirstName} {LastName}"` ذخیره شده — **نه شماره موبایل!**
### رفع
اول کاربر بر اساس `UserId` (از `ClaimTypes.NameIdentifier`) پیدا شود، سپس از `user.Mobile` برای جستجوی OTP استفاده شود:
```csharp
// ✅ بعد — ابتدا کاربر از UserId پیدا شود
var userId = long.Parse(_currentUserService.UserId);
var user = await _context.Users
.Where(x => x.Id == userId)...
var otpToken = await _context.OtpTokens
.Where(x => x.Mobile == user.Mobile ...)
```
### فایل
`CMS/src/CMSMicroservice.Application/UserCQ/Commands/AcceptContract/AcceptContractCommandHandler.cs`
---
## 🟡 باگ ۳ — `IsCompleteRegister()` داده قدیمی می‌خواند
### مشکل
متد sync در `AuthService`:
```csharp
// ❌ قبل — GetAwaiter() بدون GetResult() عملیات async را اجرا نمی‌کند
InitUserAuthInfo().GetAwaiter();
```
`GetAwaiter()` فقط یک شیء awaiter برمی‌گرداند ولی عملیات را اجرا **نمی‌کند**. در نتیجه `_userAuthInfo` مقداردهی نمی‌شود و منوی کناری (`MainLayout.razor`) وضعیت نادرست نشان می‌دهد.
### رفع
```csharp
// ✅ بعد — عملیات async را همگام اجرا می‌کند
InitUserAuthInfo().GetAwaiter().GetResult();
```
### محل استفاده
`MainLayout.razor` خطوط ۱۰۶ و ۱۰۹:
```razor
Disabled="@(!AuthService.IsCompleteRegister())"
```
### فایل
`FrontOffice/src/FrontOffice.Main/Utilities/AuthService.cs`
---
## 🏗️ معماری فلوی ثبت‌نام (بعد از فیکس)
```
┌─────────────────────────────────────────────────────────────┐
│ FrontOffice (Blazor WASM) │
│ │
│ LoginPage → SendOtp → VerifyOtp(mobile, code, referral) │
│ ↓ gRPC-Web │
├─────────────────────────────────────────────────────────────┤
│ CMS Backend (gRPC) │
│ │
│ UserContract.VerifyOtpToken │
│ ↓ │
│ UserCQ/VerifyOtpTokenCommandHandler │
│ │ │
│ ├── OTP صحیح؟ → ❌ خطا │
│ │ │
│ ├── کاربر موجود؟ → ✅ تولید JWT Token │
│ │ │
│ └── کاربر جدید؟ │
│ ├── اعتبارسنجی کد معرف │
│ ├── بررسی ظرفیت درخت باینری │
│ ├── ایجاد User + UserRole + UserWallet │
│ ├── رویدادهای دامنه │
│ └── تولید JWT Token │
│ │
│ بعد از ثبت‌نام → RegisterWizard: │
│ Step 1: اطلاعات شخصی │
│ Step 2: امضای قرارداد (AcceptContract + OTP) │
│ Step 3: خرید پکیج │
├─────────────────────────────────────────────────────────────┤
│ Database │
│ │
│ Users ─── UserRoles ─── UserWallets │
│ └── NetworkParentId, LegPosition (Binary Tree) │
│ └── UserContracts, ClubMembership │
└─────────────────────────────────────────────────────────────┘
```
---
## 📝 فایل‌های تغییر یافته
| فایل | تغییر |
|------|-------|
| `CMS/.../UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommandHandler.cs` | اضافه شدن لاجیک ایجاد کاربر جدید (86→165 خط) |
| `CMS/.../UserCQ/Commands/AcceptContract/AcceptContractCommandHandler.cs` | تغییر lookup از Username به UserId |
| `FrontOffice/.../Utilities/AuthService.cs` | اضافه شدن `.GetResult()` به `GetAwaiter()` |
---
## ✅ بیلد
```
CMS: 0 Error(s) ✅
FrontOffice: 0 Error(s) ✅
BackOffice: 0 Error(s) ✅
```
-158
View File
@@ -1,158 +0,0 @@
# کارهای باقیمانده - CMS Microservice
> آخرین بروزرسانی: February 10, 2026
> Build Status: ✅ SUCCESS (0 Errors)
---
## 📊 خلاصه وضعیت
| دسته | تعداد | وضعیت |
|------|--------|--------|
| ~~متدهای Unimplemented~~ | ~~30~~**0** | ✅ همه انجام شد |
| ~~متدهای Mock/جعلی~~ | ~~13~~**0** | ✅ همه انجام شد |
| ~~گزارش‌های TODO (صفر برمی‌گردونه)~~ | ~~2~~**0** | ✅ هر دو پیاده شد |
| ~~فایل تنظیمات اشتباه (Staging)~~ | ~~1~~**0** | ✅ فیکس شد |
| ~~Entity‌های تکراری مُرده~~ | ~~3~~**0** | ✅ حذف شد |
| **مجموع باقیمانده** | **0** | ✅ 🎉 |
---
## ✅ کارهای انجام‌شده
### فاز ۱ — فیکس‌های فوری ✅
- [x] اصلاح `appsettings.Staging.json` — URL از `backoffice-bff` به `cms` تغییر کرد
- [x] حذف `Products.cs`, `ProductImages.cs`, `ProductGalleries.cs` (Entity‌های تکراری)
- [x] اصلاح `nameof(Products)``nameof(Product)` در `GetCustomerProductsQueryHandler`
### فاز ۲ — ProductsService ✅ (8 متد)
- [x] `BulkUpdateProductPrices` — بروزرسانی قیمت/تخفیف/تخفیف باشگاه
- [x] `BulkUpdateProductStock` — بروزرسانی موجودی (SET/ADD/SUBTRACT)
- [x] `GetLowStockProducts` — محصولات کم‌موجودی با صفحه‌بندی
- [x] `ToggleProductStatus` — فعال/غیرفعال محصول
- [x] `GetProductsForCategory` — DragDrop: محصولات برای دسته‌بندی
- [x] `GetCategories` — DragDrop: دسته‌بندی‌ها برای محصول
- [x] `UpdateProductCategories` — DragDrop: بروزرسانی دسته‌بندی‌های محصول
- [x] `UpdateCategoryProducts` — DragDrop: بروزرسانی محصولات دسته‌بندی
### فاز ۳ — CityService ✅ (6 متد) + CategoryService ✅ (1 متد)
- [x] `GetCitiesForCustomer` — لیست شهرها با فیلتر و صفحه‌بندی
- [x] `GetCityByIdForCustomer` — شهر با ID
- [x] `GetCitiesByStateForCustomer` — شهرهای استان
- [x] `CreateCity` — ایجاد شهر
- [x] `UpdateCity` — بروزرسانی شهر
- [x] `DeleteCity` — حذف نرم شهر
- [x] `GetCategoryByIdForCustomer` — دسته‌بندی با ID
### فاز ۴ — UserCartsService ✅ (5 متد)
- [x] `AddNewUserCart` — افزودن به سبد خرید
- [x] `UpdateUserCart` — بروزرسانی تعداد
- [x] `DeleteUserCart` — حذف نرم
- [x] `GetUserCart` — دریافت آیتم سبد
- [x] `GetAllUserCartsByFilter` — لیست سبد خرید با فیلتر و صفحه‌بندی
### فاز ۵ — InventoryService ✅ (7 متد)
- [x] `ReserveStock` — رزرو موجودی
- [x] `ReleaseReservation` — آزادسازی رزرو
- [x] `ConfirmSale` — تأیید فروش
- [x] `ProcessReturn` — پردازش مرجوعی
- [x] `BulkAddStock` — افزودن موجودی انبوه
- [x] `GetInventorySummary` — خلاصه انبارداری (واقعی با DB)
- [x] `GetStockValueReport` — گزارش ارزش موجودی (واقعی با DB)
### فاز ۶ — UserOrderService ✅ (8 Unimplemented + 3 Mock)
- [x] `CreateNewUserOrder` — ایجاد سفارش
- [x] `UpdateUserOrder` — بروزرسانی سفارش
- [x] `DeleteUserOrder` — حذف نرم
- [x] `CancelOrder` — لغو سفارش (ادمین)
- [x] `UpdateOrderStatus` — بروزرسانی وضعیت
- [x] `GetOrdersByDateRange` — سفارشات بازه زمانی
- [x] `ApplyDiscountToOrder` — اعمال تخفیف
- [x] `CalculateOrderPV` — محاسبه PV
- [x] `CustomerCancelOrder` — لغو سفارش مشتری (با ریفاند کیف پول)
- [x] `CustomerTrackOrder` — پیگیری سفارش واقعی
- [x] `CustomerReorderPreviousOrder` — سفارش مجدد واقعی
### فاز ۷ — ConfigurationService ✅ (2 متد)
- [x] `CreateOrUpdateConfiguration``FailedPrecondition` (عمداً read-only)
- [x] `DeactivateConfiguration``FailedPrecondition` (عمداً read-only)
### فاز ۸ — UserService ✅ (5 متد Mock → واقعی)
- [x] `GetCustomerUser` — خواندن از DB با `_context.Users` + JWT userId
- [x] `UpdateCustomerProfile` — بروزرسانی FirstName/LastName/Email/NationalCode/BirthDate
- [x] `ChangeCustomerPassword` — PBKDF2 verify + hash با `IHashService`
- [x] `UploadCustomerAvatar` — ارسال به FMS با `IFileManagementService` + ذخیره URL
- [x] `UpdateCustomerSettings` — بروزرسانی EmailNotifications/SmsNotifications/PushNotifications
### فاز ۹ — TransactionsService ✅ (2 متد Mock → واقعی)
- [x] `CustomerPaymentRequest` — ایجاد Transaction + `IPaymentGatewayService.InitiatePaymentAsync`
- [x] `CustomerPaymentVerification``IPaymentGatewayService.VerifyPaymentAsync` + آپدیت Transaction
### فاز ۱۰ — PackageService ✅ (2 متد Mock → واقعی)
- [x] `CustomerPurchasePackage` — ایجاد Transaction + UserPackagePurchase + payment initiate
- [x] `CustomerVerifyPackagePurchase` — verify payment + آپدیت Transaction و Purchase
### فاز ۱۱ — UserWalletService ✅ (1 متد Mock → واقعی)
- [x] `CustomerWithdrawBalance` — آپدیت UserCommissionPayout با WithdrawalMethod/IbanNumber + Status=WithdrawRequested
---
## ✅ همه ۴۹ آیتم تکمیل شد! 🎉
> هیچ Mock یا Unimplemented متدی باقی نمانده.
---
## 📝 نکات فنی مهم
### سایر آیتم‌ها (غیر‌بحرانی)
- `Infrastructure/Services/InventoryService.cs:L546` — یک `TODO: Implement rollback logic` (در لایه Infrastructure، نه WebApi)
- `Infrastructure/Services/DayaLoanApiService.cs``MockDayaLoanApiService` (سرویس شبیه‌سازی API دایا — عمدی برای تست)
### الگوهای فنی استفاده‌شده
- **oneof در protobuf**: باید از `request.HasPaymentStatus` استفاده بشه (نه `request.PaymentStatusItem != null`)
- **StringValue wrapper**: در C# مستقیم `string` هست (بدون `.Value`)
- **Int64Value wrapper**: در C# `long?` هست (`.Value` برای unwrap)
- **DeliveryStatus**: در proto فقط ۵ مقدار (None تا Returned)، در Domain ۶ مقدار (+ Cancelled)
- **ProductType**: در C# protobuf `ProductType.Unspecified` هست (نه `ProductTypeUnspecified`)
- **ICurrentUserService.UserId**: `string?` — همیشه با `long.TryParse` تبدیل بشه
- **IPaymentGatewayService**: ثبت‌شده در DI (`DayaPaymentService` واقعی / `MockPaymentGatewayService` تست)
- **IHashService**: PBKDF2 — `HashPassword()` / `VerifyPassword()`
- **IFileManagementService**: FMS gRPC — `UploadFileAsync(dir, bytes, mime, name, ct)`
---
## 📋 ترتیب انجام کارها (تکمیل‌شده)
- [x] فاز ۱ — فیکس‌های فوری (Staging URL, Dead Entities)
- [x] فاز ۲ — ProductsService (8 متد)
- [x] فاز ۳ — CityService (6 متد) + CategoryService (1 متد)
- [x] فاز ۴ — UserCartsService (5 متد)
- [x] فاز ۵ — InventoryService (7 متد)
- [x] فاز ۶ — UserOrderService (8 Unimplemented + 3 Mock)
- [x] فاز ۷ — ConfigurationService (2 متد)
- [x] فاز ۸ — UserService (5 Mock → واقعی)
- [x] فاز ۹ — TransactionsService (2 Mock → واقعی)
- [x] فاز ۱۰ — PackageService (2 Mock → واقعی)
- [x] فاز ۱۱ — UserWalletService (1 Mock → واقعی)
---
## ✅ تاریخچه انجام کارها
| تاریخ | کار | وضعیت |
|-------|------|--------|
| Dec 2025 | مهاجرت ۲۰/۲۰ سرویس BackOffice BFF→CMS | ✅ |
| Jan 2026 | فعال‌سازی ماژول‌های DiscountShop | ✅ |
| Feb 2026 | یکپارچه‌سازی FMS (آپلود فایل با ImageSharp) | ✅ |
| Feb 2026 | رفع باگ OTP SMS (Kavenegar) | ✅ |
| Feb 2026 | رفع باگ BCrypt Invalid Salt Version | ✅ |
| Feb 2026 | شناسایی مشکل Token/Roles (`_Imports.razor`) | ✅ |
| Feb 2026 | پیاده‌سازی ۳۰ متد Unimplemented | ✅ |
| Feb 2026 | جایگزینی ۳ متد Mock (UserOrder مشتری) | ✅ |
| Feb 2026 | فیکس Staging URL, Dead Entities, Build Errors | ✅ |
| Feb 2026 | جایگزینی ۵ متد Mock (UserService) — DB+JWT+FMS+Hash | ✅ |
| Feb 2026 | جایگزینی ۲ متد Mock (TransactionsService) — PaymentGateway | ✅ |
| Feb 2026 | جایگزینی ۲ متد Mock (PackageService) — Purchase+Verify | ✅ |
| Feb 2026 | جایگزینی ۱ متد Mock (UserWalletService) — Withdraw | ✅ |
| Feb 2026 | **همه ۴۹/۴۹ آیتم تکمیل — Build بدون خطا** | ✅ 🎉 |
-583
View File
@@ -1,583 +0,0 @@
# ساده‌سازی سیستم مدیریت صفحات سایت
> **تاریخ:** ۱۳۹۶/۱۱/۲۸ (2026-02-17)
> **وضعیت:** ✅ پیاده‌سازی کامل شده — مرج به production
> **اولویت:** بالا
---
## ۱. خلاصه مسئله
### وضعیت فعلی (مشکلات)
سیستم فعلی مدیریت صفحات **بیش از حد انعطاف‌پذیر و پیچیده** طراحی شده:
| مشکل | توضیح |
|-------|--------|
| **سیستم عمومی Section/Key** | ادمین باید `SectionKey` رو دقیق تایپ کنه (مثلاً `value-1`, `team-2`). یه اشتباه تایپی باعث میشه فرانت اون بخش رو پیدا نکنه |
| **ExtraData به‌صورت JSON خام** | اطلاعات تماس (آدرس/تلفن/ایمیل) و شبکه‌های اجتماعی داخل textarea به JSON خام نوشته میشه — خطاپذیر |
| **HTML Editor برای همه‌چیز** | حتی برای متن‌های ساده (عنوان یک Value) از HTML Editor استفاده میشه |
| **CRUD نامحدود** | ادمین میتونه صفحات جدید بسازه ولی فرانت فقط `about` و `contact` رو میشناسه |
| **لندینگ پیج کاملاً هاردکد** | محتوای لندینگ (Steps, Features, Stats, FAQ, Testimonials) داخل کد C# هاردکد شده و از CMS استفاده نمیکنه |
| **صفحه مجوزها وجود نداره** | هیچ صفحه‌ای برای نمایش نمادهای اعتماد و مجوزها نداریم |
### هدف
**۴ صفحه مشخص** با **فرم‌های اختصاصی** (نه عمومی) در پنل ادمین:
| # | صفحه | محتوای قابل ویرایش | طراحی |
|---|-------|-------------------|--------|
| 1 | **لندینگ** | عنوان hero، زیرعنوان، متن دکمه‌ها، عناوین سکشن‌ها، متن مراحل/ویژگی‌ها/سوالات/آمار | **چیدمان ثابت** — فقط متن‌ها قابل تغییر |
| 2 | **درباره ما** | عنوان، چشم‌انداز، مأموریت، ارزش‌ها (عنوان+متن+آیکون)، اعضای تیم (نام+سمت+تصویر) | **چیدمان ثابت** — تعداد ارزش‌ها و اعضا قابل تغییر |
| 3 | **تماس با ما** | آدرس، تلفن، ایمیل، ساعات کاری، لینک شبکه‌های اجتماعی | **چیدمان ثابت** — فقط اطلاعات قابل تغییر |
| 4 | **مجوزها** 🆕 | تصاویر مجوزها با عنوان و لینک (نماد اعتماد الکترونیکی و ...) | **ساده و یکپارچه** |
---
## ۲. معماری جدید — Structured Page Settings
### فلسفه طراحی
```
❌ قبلی: Generic Sections + Free-form Keys + Raw JSON
✅ جدید: Typed Settings per Page + Structured Forms + Fixed Layout
```
به‌جای اینکه هر صفحه N تا Section داشته باشه با Key‌های دلخواه، **هر صفحه یک مدل مشخص** با فیلدهای تایپ‌شده داره.
### ۲.۱ مدل‌های داده جدید (Database)
#### جدول `SitePageSettings` (جایگزین SitePage + SitePageSection)
```
┌─────────────────────────────────────────────┐
│ SitePageSettings │
├─────────────────────────────────────────────┤
│ Id : long (PK) │
│ PageKey : string(50) [UNIQUE INDEX] │ ← "landing" | "about" | "contact" | "licenses"
│ Title : string(200) │
│ MetaDescription : string?(300) │
│ HeroTitle : string?(200) │
│ HeroSubtitle : string?(500) │
│ HeroImagePath : string? │
│ IsActive : bool │
│ SettingsJson : string (JSON Column) │ ← ⭐ Typed JSON per PageKey
│ + Audit fields │
└─────────────────────────────────────────────┘
```
#### جدول `SitePageImage` (برای مجوزها و تصاویر تیم)
```
┌─────────────────────────────────────────────┐
│ SitePageImage │
├─────────────────────────────────────────────┤
│ Id : long (PK) │
│ SitePageSettingsId : long (FK) │
│ ImageGroup : string(50) │ ← "licenses" | "team" | "values"
│ Title : string?(200) │
│ Subtitle : string?(300) │
│ Description : string? │
│ ImagePath : string │
│ ThumbnailPath : string? │
│ LinkUrl : string? │ ← برای مجوزها: لینک به سایت مرجع
│ IconName : string?(100) │
│ SortOrder : int │
│ IsActive : bool │
│ + Audit fields │
└─────────────────────────────────────────────┘
```
#### SettingsJson — ساختار به‌ازای هر صفحه
**Landing (`PageKey = "landing"`):**
```json
{
"heroButtonPrimaryText": "شروع کنید",
"heroButtonSecondaryText": "بیشتر بدانید",
"steps": [
{ "title": "ثبت‌نام", "description": "...", "iconName": "PersonAdd" }
],
"features": [
{ "title": "پشتیبانی ۲۴/۷", "description": "...", "iconName": "Support" }
],
"stats": [
{ "label": "کاربران فعال", "value": 15000, "suffix": "+" }
],
"testimonials": [
{ "name": "علی محمدی", "role": "کاربر", "text": "...", "rating": 5 }
],
"faqs": [
{ "question": "سوال ۱", "answer": "جواب ۱", "category": "عمومی" }
],
"featuredBlogEnabled": true,
"ctaTitle": "همین الان شروع کنید",
"ctaDescription": "...",
"ctaButtonText": "ثبت‌نام رایگان"
}
```
**About (`PageKey = "about"`):**
```json
{
"visionTitle": "چشم‌انداز",
"visionText": "...",
"missionTitle": "مأموریت",
"missionText": "...",
"valuesTitle": "ارزش‌های ما",
"teamTitle": "تیم ما"
}
```
+ `SitePageImage` records با `ImageGroup = "values"` برای ارزش‌ها
+ `SitePageImage` records با `ImageGroup = "team"` برای اعضای تیم
**Contact (`PageKey = "contact"`):**
```json
{
"address": "تهران، ...",
"phone": "021-12345678",
"email": "info@kbs1.ir",
"workingHours": "شنبه تا چهارشنبه ۹ تا ۱۸",
"telegramUrl": "https://t.me/...",
"instagramUrl": "https://instagram.com/...",
"linkedinUrl": "https://linkedin.com/...",
"whatsappUrl": "https://wa.me/...",
"mapLatitude": 35.6892,
"mapLongitude": 51.3890,
"formSubjects": ["پشتیبانی فنی", "پیشنهاد همکاری", "سایر"]
}
```
**Licenses (`PageKey = "licenses"`):**
```json
{
"pageDescription": "مجوزها و نمادهای اعتماد",
"displayStyle": "grid"
}
```
+ `SitePageImage` records با `ImageGroup = "licenses"` — هر مجوز: Title + ImagePath + LinkUrl
---
## ۳. تغییرات به‌ازای هر لایه
### ۳.۱ دیتابیس (CMS — Entity Framework)
| عملیات | فایل/محل | توضیح |
|--------|----------|--------|
| **حذف** | `SitePage` entity | جایگزین با `SitePageSettings` |
| **حذف** | `SitePageSection` entity | جایگزین با `SitePageImage` (فقط برای آیتم‌های تصویری) |
| **ایجاد** | `SitePageSettings.cs` | Entity جدید با `SettingsJson` |
| **ایجاد** | `SitePageImage.cs` | Entity جدید برای تصاویر (مجوزها، تیم، ارزش‌ها) |
| **تغییر** | `DbContext``SitePageConfiguration` | Configuration جدید |
| **تغییر** | `SitePageSeedData.cs` | Seed data جدید برای ۴ صفحه |
| **Migration** | EF Migration | **Data migration** از ساختار قدیم به جدید |
### ۳.۲ Proto / gRPC Contract
| عملیات | توضیح |
|--------|--------|
| **بازنویسی** | `site_pages.proto` — حذف ۱۰ RPC قبلی، جایگزین با ۴ RPC ساده |
```protobuf
service SitePageSettingsService {
// دریافت تنظیمات صفحه با کلید (FrontOffice)
rpc GetPageSettings (GetPageSettingsRequest) returns (PageSettingsResponse);
// ذخیره تنظیمات صفحه (BackOffice — Admin)
rpc SavePageSettings (SavePageSettingsRequest) returns (SavePageSettingsResponse);
// مدیریت تصاویر صفحه (مجوزها، تیم، ارزش‌ها)
rpc SavePageImage (SavePageImageRequest) returns (SavePageImageResponse);
rpc DeletePageImage (DeletePageImageRequest) returns (DeletePageImageResponse);
// لیست همه صفحات (BackOffice)
rpc GetAllPages (GetAllPagesRequest) returns (GetAllPagesResponse);
}
```
### ۳.۳ CMS Backend (Application Layer)
| عملیات | فایل‌ها | توضیح |
|--------|---------|--------|
| **حذف** | ۲۲ فایل Command/Query فعلی | CQRS handlers قدیمی |
| **ایجاد** | `GetPageSettingsQuery` + Handler | دریافت Settings + Images |
| **ایجاد** | `SavePageSettingsCommand` + Handler + Validator | ذخیره JSON با اعتبارسنجی |
| **ایجاد** | `SavePageImageCommand` + Handler | آپلود/ویرایش تصویر |
| **ایجاد** | `DeletePageImageCommand` + Handler | حذف تصویر |
| **ایجاد** | `GetAllPagesQuery` + Handler | لیست صفحات |
| **تغییر** | gRPC Service | `SitePageGrpcService` بازنویسی |
### ۳.۴ FrontOffice (Blazor Server — سمت مشتری)
| عملیات | فایل | توضیح |
|--------|------|--------|
| **تغییر** | `SitePageService.cs` | ساده‌سازی — فقط `GetPageSettings(pageKey)` |
| **تغییر** | `Index.razor` + `.cs` | **بزرگ‌ترین تغییر:** از هاردکد به CMS-driven. چیدمان ثابت میمونه، فقط متن‌ها از `SettingsJson` خونده میشه |
| **تغییر** | `AboutUs.razor` + `.cs` | ساده‌تر — خواندن مستقیم فیلدهای typed به‌جای key-matching |
| **تغییر** | `ContactUs.razor` + `.cs` | ساده‌تر — خواندن مستقیم فیلدها بدون JSON parsing |
| **ایجاد** | `Licenses.razor` + `.cs` | 🆕 صفحه جدید مجوزها |
### ۳.۵ BackOffice (Blazor WASM — پنل ادمین)
| عملیات | فایل | توضیح |
|--------|------|--------|
| **حذف** | ۵ فایل Dialog فعلی | `CreateSitePageDialog`, `EditSitePageDialog`, `SitePageSectionsDialog`, `SitePageSectionEditDialog` |
| **حذف** | `SitePageManagement.razor` + `.cs` فعلی | جایگزین |
| **ایجاد** | `PageSettingsManagement.razor` | صفحه اصلی — لیست ۴ صفحه ثابت |
| **ایجاد** | `LandingPageSettings.razor` | 🌟 فرم اختصاصی لندینگ |
| **ایجاد** | `AboutPageSettings.razor` | 🌟 فرم اختصاصی درباره‌ما |
| **ایجاد** | `ContactPageSettings.razor` | 🌟 فرم اختصاصی تماس |
| **ایجاد** | `LicensesPageSettings.razor` | 🌟 فرم اختصاصی مجوزها |
| **تغییر** | `ISitePageService` + Impl | ساده‌سازی interface |
---
## ۴. طراحی UI پنل ادمین (BackOffice)
### ۴.۱ صفحه اصلی مدیریت صفحات
```
┌─────────────────────────────────────────────────────────┐
│ مدیریت صفحات سایت │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 🏠 │ │ 👥 │ │ 📞 │ │ 🏅 │ │
│ │ لندینگ │ │ درباره‌ما│ │ تماس │ │ مجوزها │ │
│ │ │ │ │ │ │ │ │ │
│ │ [ویرایش] │ │ [ویرایش] │ │ [ویرایش] │ │ [ویرایش] │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ℹ️ صفحات سایت ثابت هستند و فقط محتوای آنها │
│ قابل ویرایش است │
└─────────────────────────────────────────────────────────┘
```
### ۴.۲ فرم ویرایش لندینگ (نمونه)
```
┌─────────────────────────────────────────────────────────┐
│ ← بازگشت تنظیمات صفحه لندینگ │
├─────────────────────────────────────────────────────────┤
│ │
│ ── Hero Section ────────────────────────────────────── │
│ عنوان: [_______________________________] │
│ زیرعنوان: [_______________________________] │
│ تصویر پس‌زمینه: [📎 انتخاب فایل] │
│ متن دکمه اصلی: [_____________] │
│ متن دکمه ثانویه:[_____________] │
│ │
│ ── مراحل (Steps) ───────────────────────────────────── │
│ ┌────┬──────────────┬──────────────────┬────────┐ │
│ │ # │ عنوان │ توضیح │ آیکون │ │
│ ├────┼──────────────┼──────────────────┼────────┤ │
│ │ 1 │ [ثبت‌نام ] │ [در کمتر از...] │ [🔍] │ │
│ │ 2 │ [دعوت ] │ [لینک اختصاصی ] │ [🔍] │ │
│ │ 3 │ [دریافت ] │ [پاداش‌های... ] │ [🔍] │ │
│ │ │ │ [+ افزودن مرحله]│ │ │
│ └────┴──────────────┴──────────────────┴────────┘ │
│ │
│ ── ویژگی‌ها (Features) ──────────────────────────────── │
│ (مشابه بالا — جدول قابل ویرایش) │
│ │
│ ── آمار (Stats) ────────────────────────────────────── │
│ ┌──────────────┬─────────┬────────┐ │
│ │ برچسب │ مقدار │ پسوند │ │
│ ├──────────────┼─────────┼────────┤ │
│ │ [کاربران ] │ [15000] │ [+] │ │
│ └──────────────┴─────────┴────────┘ │
│ │
│ ── نظرات مشتریان ───────────────────────────────────── │
│ (جدول: نام، نقش، متن، امتیاز) │
│ │
│ ── سوالات متداول (FAQ) ─────────────────────────────── │
│ (جدول: سوال، جواب، دسته‌بندی) │
│ │
│ ── CTA Banner ──────────────────────────────────────── │
│ عنوان: [_______________] │
│ توضیح: [_______________] │
│ متن دکمه: [_______________] │
│ │
│ [💾 ذخیره تغییرات] │
└─────────────────────────────────────────────────────────┘
```
### ۴.۳ فرم ویرایش تماس با ما
```
┌─────────────────────────────────────────────────────────┐
│ ← بازگشت تنظیمات صفحه تماس با ما │
├─────────────────────────────────────────────────────────┤
│ │
│ ── اطلاعات تماس ───────────────────────────────────── │
│ آدرس: [_______________________________] │
│ تلفن: [_______________________________] │
│ ایمیل: [_______________________________] │
│ ساعات کاری: [_______________________________] │
│ │
│ ── شبکه‌های اجتماعی ───────────────────────────────── │
│ تلگرام: [_______________________________] │
│ اینستاگرام: [_______________________________] │
│ لینکدین: [_______________________________] │
│ واتس‌اپ: [_______________________________] │
│ │
│ ── تنظیمات فرم تماس ───────────────────────────────── │
│ موضوعات: [پشتیبانی فنی ×] [پیشنهاد همکاری ×] │
│ [+ افزودن موضوع] │
│ │
│ [💾 ذخیره تغییرات] │
└─────────────────────────────────────────────────────────┘
```
### ۴.۴ فرم مجوزها (جدید)
```
┌─────────────────────────────────────────────────────────┐
│ ← بازگشت مدیریت مجوزها و نمادهای اعتماد │
├─────────────────────────────────────────────────────────┤
│ │
│ توضیح صفحه: [_______________________________] │
│ │
│ ── مجوزها ──────────────────────────────────────────── │
│ ┌──────┬──────────────┬─────────────────┬────────────┐ │
│ │ تصویر│ عنوان │ لینک │ عملیات │ │
│ ├──────┼──────────────┼─────────────────┼────────────┤ │
│ │ [🖼] │ [نماد اعتماد]│ [https://...] │ [🗑] [↕] │ │
│ │ [🖼] │ [ساماندهی ] │ [https://...] │ [🗑] [↕] │ │
│ │ [🖼] │ [مجوز کسب..] │ [https://...] │ [🗑] [↕] │ │
│ └──────┴──────────────┴─────────────────┴────────────┘ │
│ │
│ [+ افزودن مجوز جدید] │
│ │
│ [💾 ذخیره تغییرات] │
└─────────────────────────────────────────────────────────┘
```
---
## ۵. طراحی UI فرانت مشتری (FrontOffice)
### ۵.۱ صفحه مجوزها (جدید — `/licenses`)
```
┌─────────────────────────────────────────────────────────┐
│ [Header / Navbar] │
├─────────────────────────────────────────────────────────┤
│ │
│ 🏅 مجوزها و نمادهای اعتماد │
│ توضیح کوتاه صفحه از CMS │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ │ │ │ │ │ │
│ │ [تصویر] │ │ [تصویر] │ │ [تصویر] │ │
│ │ │ │ │ │ │ │
│ │ نماد │ │ ساماندهی │ │ مجوز │ │
│ │ اعتماد │ │ │ │ کسب‌وکار │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ (هر تصویر لینک‌دار — کلیک = باز شدن سایت مرجع) │
│ │
├─────────────────────────────────────────────────────────┤
│ [Footer] │
└─────────────────────────────────────────────────────────┘
```
### ۵.۲ تغییرات سایر صفحات
- **لندینگ:** بدون تغییر ظاهری — فقط منبع داده از هاردکد به CMS تغییر میکنه
- **درباره ما:** بدون تغییر ظاهری — کد ساده‌تر میشه
- **تماس با ما:** بدون تغییر ظاهری — کد ساده‌تر میشه
---
## ۶. Migration Plan — مراحل اجرا
### فاز ۱: دیتابیس و Backend (CMS)
```
مدت تخمینی: ۱ روز
```
| # | تسک | جزئیات |
|---|------|--------|
| 1.1 | ایجاد Entity‌های جدید | `SitePageSettings`, `SitePageImage` |
| 1.2 | ایجاد EF Configuration | Indexes, relationships, JSON column |
| 1.3 | نوشتن Migration | `SimplifySitePages` — ایجاد جدول‌های جدید |
| 1.4 | نوشتن Data Migration | انتقال داده از `SitePage`/`SitePageSection` به ساختار جدید |
| 1.5 | Seed Data جدید | ۴ صفحه: landing, about, contact, licenses |
| 1.6 | حذف Entity‌های قدیمی | بعد از تأیید migration موفق |
### فاز ۲: Proto و gRPC Service
```
مدت تخمینی: ۰.۵ روز
```
| # | تسک | جزئیات |
|---|------|--------|
| 2.1 | بازنویسی `site_pages.proto` | ۵ RPC جدید (Get, Save, Image CRUD, List) |
| 2.2 | بازنویسی gRPC Service | `SitePageSettingsGrpcService` |
| 2.3 | نوشتن CQRS Handlers | ۵ handler جدید |
| 2.4 | Validators | اعتبارسنجی SettingsJson بر اساس PageKey |
### فاز ۳: BackOffice (پنل ادمین)
```
مدت تخمینی: ۱.۵ روز
```
| # | تسک | جزئیات |
|---|------|--------|
| 3.1 | حذف UI قدیمی | ۵ فایل dialog + management page |
| 3.2 | صفحه اصلی | `PageSettingsManagement.razor` — کارت‌های ۴ صفحه |
| 3.3 | فرم لندینگ | `LandingPageSettings.razor` — فرم با سکشن‌های Steps/Features/Stats/FAQ/Testimonials/CTA |
| 3.4 | فرم درباره‌ما | `AboutPageSettings.razor` — Vision/Mission + مدیریت Values & Team |
| 3.5 | فرم تماس | `ContactPageSettings.razor` — فیلدهای ساده |
| 3.6 | فرم مجوزها | `LicensesPageSettings.razor` — آپلود و مدیریت تصاویر مجوز |
| 3.7 | سرویس BackOffice | `ISitePageSettingsService` + Implementation |
### فاز ۴: FrontOffice (سمت مشتری)
```
مدت تخمینی: ۱ روز
```
| # | تسک | جزئیات |
|---|------|--------|
| 4.1 | بروزرسانی `SitePageService` | ساده‌سازی — فقط `GetPageSettings` |
| 4.2 | بروزرسانی `Index.razor` | خواندن Steps/Features/Stats/FAQ/Testimonials از CMS |
| 4.3 | بروزرسانی `AboutUs.razor` | خواندن مستقیم فیلدها (بدون key-matching) |
| 4.4 | بروزرسانی `ContactUs.razor` | خواندن مستقیم فیلدها (بدون JSON parsing) |
| 4.5 | ایجاد `Licenses.razor` | 🆕 صفحه جدید مجوزها |
| 4.6 | افزودن به Navigation | لینک مجوزها در Footer |
### فاز ۵: تست و Cleanup
```
مدت تخمینی: ۰.۵ روز
```
| # | تسک | جزئیات |
|---|------|--------|
| 5.1 | تست E2E | همه ۴ صفحه در FrontOffice |
| 5.2 | تست ادمین | ویرایش همه ۴ صفحه از BackOffice |
| 5.3 | حذف کدهای قدیمی | فایل‌هایی که دیگه استفاده نمیشن |
| 5.4 | بروزرسانی مستندات | CHANGELOG, INDEX.md |
---
## ۷. مقایسه قبل و بعد
### کاهش پیچیدگی
| معیار | قبل | بعد | تغییر |
|-------|------|------|--------|
| RPC‌های gRPC | 10 | 5 | -50% |
| CQRS Handlers | 10 (22 file) | 5 (~12 file) | -45% |
| Entity‌ها | 2 (generic) | 2 (typed) | = |
| BackOffice Dialogs | 4 generic | 4 specific | کیفیت↑ |
| JSON خام در UI | ✅ بله | ❌ خیر | حذف |
| خطای تایپ SectionKey | ✅ ممکن | ❌ غیرممکن | حذف |
| لندینگ CMS-driven | ❌ هاردکد | ✅ CMS | بهبود |
| صفحه مجوزها | ❌ ندارد | ✅ دارد | 🆕 |
### تجربه ادمین
| قبل | بعد |
|------|------|
| لیست صفحات → انتخاب → مدیریت سکشن‌ها → ویرایش سکشن (4 مرحله) | ۴ کارت → فرم اختصاصی (2 مرحله) |
| JSON خام برای اطلاعات تماس | فیلدهای مشخص (آدرس، تلفن، ایمیل) |
| HTML Editor برای عنوان ساده | Text field ساده |
| امکان ساخت صفحه‌ای که فرانت نمیشناسه | فقط ۴ صفحه مشخص |
---
## ۸. ریسک‌ها و ملاحظات
| ریسک | شدت | راه‌حل |
|------|------|--------|
| Data migration از ساختار قدیم | متوسط | Script migration دقیق + بکاپ قبل از اجرا |
| Breaking change در Proto | بالا | نسخه جدید Proto NuGet + بروزرسانی هر ۳ پروژه همزمان |
| تصاویر موجود (hero, section images) | کم | مسیرها در فایل سیستم ثابت میمونن — فقط reference DB تغییر میکنه |
| Backward compatibility | کم | چون ساختار قبلی فقط ۲ صفحه فعال داشت، migration ساده‌ست |
---
## ۹. فایل‌های تأثیرپذیر (خلاصه)
### حذف (Delete)
```
CMS:
- Domain/Entities/Content/SitePage.cs
- Domain/Entities/Content/SitePageSection.cs
- Application/Features/SitePages/* (22 files)
- Infrastructure/Persistence/Configurations/SitePageConfiguration.cs
- Infrastructure/Persistence/Configurations/SitePageSectionConfiguration.cs
BackOffice:
- Pages/Content/SitePageManagement.razor + .cs
- Pages/Content/CreateSitePageDialog.razor + .cs
- Pages/Content/EditSitePageDialog.razor + .cs
- Pages/Content/SitePageSectionsDialog.razor + .cs
- Pages/Content/SitePageSectionEditDialog.razor + .cs
```
### ایجاد (Create)
```
CMS:
- Domain/Entities/Content/SitePageSettings.cs
- Domain/Entities/Content/SitePageImage.cs
- Application/Features/SitePageSettings/* (~12 files)
- Infrastructure/Persistence/Configurations/SitePageSettingsConfiguration.cs
- Infrastructure/Persistence/Configurations/SitePageImageConfiguration.cs
BackOffice:
- Pages/Content/PageSettingsManagement.razor + .cs
- Pages/Content/LandingPageSettings.razor + .cs
- Pages/Content/AboutPageSettings.razor + .cs
- Pages/Content/ContactPageSettings.razor + .cs
- Pages/Content/LicensesPageSettings.razor + .cs
FrontOffice:
- Pages/Licenses.razor + .cs
Proto:
- site_pages.proto (rewrite)
```
### تغییر (Modify)
```
CMS:
- ApplicationDbContext.cs (DbSets)
- SitePageSeedData.cs
- SitePageGrpcService.cs
BackOffice:
- Services/ISitePageService.cs → ISitePageSettingsService.cs
- Services/SitePageService.cs → SitePageSettingsService.cs
- NavMenu (routing)
FrontOffice:
- Services/SitePageService.cs (simplify)
- Pages/Index.razor + .cs (CMS-driven)
- Pages/AboutUs.razor + .cs (simplify)
- Pages/ContactUs.razor + .cs (simplify)
- Shared/NavMenu or Footer (add Licenses link)
- DI registration
```
---
## ۱۰. نتیجه‌گیری
این تغییر یک **ساده‌سازی معماری** هست که:
1. ✅ پیچیدگی غیرضروری رو حذف میکنه
2. ✅ تجربه ادمین رو بهبود میده (فرم‌های اختصاصی به‌جای فرم‌های عمومی)
3. ✅ خطاهای انسانی رو کاهش میده (بدون JSON خام و SectionKey تایپی)
4. ✅ لندینگ پیج رو CMS-driven میکنه
5. ✅ صفحه مجوزها رو اضافه میکنه
6. ✅ حجم کد رو ~۳۰٪ کاهش میده
> **مرحله بعد:** بعد از تأیید این طرح، شروع پیاده‌سازی از فاز ۱ (دیتابیس)
-424
View File
@@ -1,424 +0,0 @@
# 🤖 Chatika Integration Guide
> **آخرین بروزرسانی**: ۳ دی ۱۴۰۴ (23 December 2025)
> **وضعیت**: ✅ Production Ready
---
## 📋 فهرست
1. [معرفی](#معرفی)
2. [معماری](#معماری)
3. [API چتیکا](#api-چتیکا)
4. [پیاده‌سازی](#پیاده‌سازی)
5. [تنظیمات](#تنظیمات)
6. [نحوه کار Worker](#نحوه-کار-worker)
7. [Troubleshooting](#troubleshooting)
---
## معرفی
چتیکا یک سرویس هوش مصنوعی است که به عنوان اولین فیچر باشگاه مشتریان به کاربران ارائه می‌شود. هنگام فعال‌سازی باشگاه، به صورت خودکار یک حساب در چتیکا برای کاربر ایجاد می‌شود.
### ویژگی‌ها:
- ✅ فعال‌سازی خودکار حساب
- ✅ جلوگیری از ثبت تکراری
- ✅ Retry با Exponential Backoff
- ✅ Logging کامل
---
## معماری
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ User Activates │───▶│ ClubMembership │───▶│ UserClubFeature │
│ Club Package │ │ (IsActive=true) │ │ (Chatika, Id=1)│
└─────────────────┘ └──────────────────┘ │ Notes = NULL │
└────────┬────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Hangfire Scheduler │
│ Cron: */5 * * * * (Every 5 minutes) │
└─────────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ ChatikaAccountActivationJob │
│ │
│ Query: SELECT * FROM UserClubFeatures │
│ WHERE ClubFeatureId = 1 (Chatika) │
│ AND ClubMembership.IsActive = true │
│ AND Notes IS NULL │
└─────────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ ChatikaApiService │
│ POST https://api.chatika.ir/api/v1/organizations/register-user │
│ Header: X-API-Key: {ApiKey} │
│ Body: { "mobile_number": "09123456789" } │
└─────────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Update UserClubFeature │
│ Notes = "🎉 تبریک! حساب هوش مصنوعی چتیکا شما فعال شد..." │
│ IsActive = true │
└─────────────────────────────────────────────────────────────────┘
```
---
## API چتیکا
### Endpoint
```
POST /api/v1/organizations/register-user
```
### Headers
| Header | Value |
|--------|-------|
| `X-API-Key` | Organization API Key |
| `Content-Type` | `application/json` |
### Request Body
```json
{
"mobile_number": "09123456789"
}
```
### Success Response (200 OK)
```json
{
"id": 1,
"mobile_number": "09123456789",
"organization_id": 1,
"organization_title": "FourSat",
"wallet_balance": 100.0,
"is_new_user": true,
"credit_charged": 100.0
}
```
### Error Responses
| Status | Error Code | Description |
|--------|-----------|-------------|
| 401 | `INVALID_API_KEY` | API Key نامعتبر |
| 403 | `ORGANIZATION_DISABLED` | سازمان غیرفعال شده |
| 403 | `ORGANIZATION_EXPIRED` | سازمان منقضی شده |
| 400 | `INVALID_MOBILE_FORMAT` | فرمت شماره موبایل نامعتبر |
---
## پیاده‌سازی
### 1. Interface
**فایل**: `CMSMicroservice.Application/Common/Interfaces/IChatikaApiService.cs`
```csharp
public interface IChatikaApiService
{
Task<ChatikaAccountResult> CreateAccountAsync(
string mobileNumber,
string fullName,
CancellationToken cancellationToken = default);
}
public class ChatikaAccountResult
{
public bool IsSuccess { get; set; }
public string? ErrorMessage { get; set; }
public string? ChatikaUserId { get; set; }
public string? AccessUrl { get; set; }
public static ChatikaAccountResult Success(...) => ...;
public static ChatikaAccountResult Failure(string error) => ...;
}
```
### 2. Service Implementation
**فایل**: `CMSMicroservice.Infrastructure/Services/ChatikaApiService.cs`
```csharp
public class ChatikaApiService : IChatikaApiService
{
private readonly HttpClient _httpClient;
private readonly ILogger<ChatikaApiService> _logger;
public async Task<ChatikaAccountResult> CreateAccountAsync(
string mobileNumber,
string fullName,
CancellationToken cancellationToken = default)
{
var request = new { mobile_number = mobileNumber };
var response = await _httpClient.PostAsJsonAsync(
"/api/v1/organizations/register-user",
request,
cancellationToken);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<ChatikaRegisterResponse>();
return ChatikaAccountResult.Success(result?.Id.ToString(), "https://chatika.ir");
}
return ChatikaAccountResult.Failure($"Error: {response.StatusCode}");
}
}
```
### 3. Background Job
**فایل**: `CMSMicroservice.Infrastructure/BackgroundJobs/ChatikaAccountActivationJob.cs`
```csharp
public class ChatikaAccountActivationJob
{
private const string ChatikaFeatureDescription =
"🎉 تبریک! حساب هوش مصنوعی چتیکا شما فعال شد.\n\n" +
"برای استفاده از امکانات رایگان چتیکا:\n" +
"1️⃣ به وب‌سایت chatika.ir مراجعه کنید\n" +
"2️⃣ شماره موبایل خود را وارد کنید\n" +
"3️⃣ از دستیار هوشمند چتیکا لذت ببرید!\n\n" +
"🔗 لینک ورود: https://chatika.ir";
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
{
// 1. پیدا کردن کاربران در انتظار
var pendingUsers = await _context.UserClubFeatures
.Include(ucf => ucf.User)
.Include(ucf => ucf.ClubMembership)
.Where(ucf =>
ucf.ClubFeatureId == (long)ClubFeatureType.Chatika &&
ucf.ClubMembership.IsActive &&
!ucf.IsDeleted &&
ucf.IsActive &&
(ucf.Notes == null || ucf.Notes == ""))
.ToListAsync(cancellationToken);
// 2. پردازش هر کاربر
foreach (var userFeature in pendingUsers)
{
var user = userFeature.User;
var fullName = $"{user.FirstName} {user.LastName}".Trim();
// 3. کال API با Retry
var result = await _retryPipeline.ExecuteAsync(
async ct => await _chatikaApiService.CreateAccountAsync(
user.Mobile, fullName, ct),
cancellationToken);
// 4. آپدیت فیچر
if (result.IsSuccess)
{
userFeature.Notes = ChatikaFeatureDescription;
userFeature.IsActive = true;
await _context.SaveChangesAsync(cancellationToken);
}
}
}
}
```
---
## تنظیمات
### appsettings.json
```json
{
"Chatika": {
"BaseUrl": "https://api.chatika.ir",
"ApiKey": "YOUR_ORGANIZATION_API_KEY"
}
}
```
### DI Registration
**فایل**: `ConfigureServices.cs`
```csharp
// Chatika API Service
services.AddHttpClient<IChatikaApiService, ChatikaApiService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.ConfigureHttpClient((sp, client) =>
{
client.Timeout = TimeSpan.FromSeconds(30);
});
// Background Job
services.AddScoped<ChatikaAccountActivationJob>();
```
### Hangfire Registration
**فایل**: `Program.cs`
```csharp
// Chatika Account Activation: Every 5 minutes
recurringJobManager.AddOrUpdate<ChatikaAccountActivationJob>(
recurringJobId: "chatika-account-activation",
methodCall: job => job.ExecuteAsync(CancellationToken.None),
cronExpression: "*/5 * * * *",
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
```
---
## نحوه کار Worker
### Flowchart
```
┌──────────────────────────────────────────────────────────────┐
│ START (Every 5 min) │
└──────────────────────────┬───────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Query: Users with Chatika feature & Notes = NULL │
└──────────────────────────┬───────────────────────────────────┘
┌─────────────┐
│ Any Users? │
└──────┬──────┘
┌────────────┴────────────┐
│ NO │ YES
▼ ▼
┌──────────┐ ┌───────────────┐
│ END │ │ For each user │
└──────────┘ └───────┬───────┘
┌────────────────────┐
│ Call Chatika API │
│ (with 3x Retry) │
└────────┬───────────┘
┌─────────┴─────────┐
│ SUCCESS │ FAILURE
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Update Notes │ │ Log Warning │
│ IsActive=true │ │ Continue │
└───────────────┘ └───────────────┘
┌────────────────┐
│ Next User │
└────────────────┘
```
### Retry Policy
```csharp
// Polly Retry: 3 attempts with exponential backoff
_retryPipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(30),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true
})
.Build();
```
**Retry Timeline:**
- Attempt 1: Immediate
- Attempt 2: ~30 seconds later
- Attempt 3: ~60 seconds later
---
## Troubleshooting
### 1. API Key Invalid
**خطا**: `INVALID_API_KEY`
**راه‌حل**:
1. بررسی `appsettings.json`
2. تأیید API Key در داشبورد چتیکا
3. چک کردن header name: باید `X-API-Key` باشد
### 2. Users Not Being Processed
**علت احتمالی**:
1. `ClubMembership.IsActive = false`
2. `UserClubFeature.Notes` قبلاً پر شده
3. `ClubFeatureId != 1`
**Debug Query**:
```sql
SELECT ucf.*, u.Mobile, cm.IsActive
FROM UserClubFeatures ucf
JOIN Users u ON ucf.UserId = u.Id
JOIN ClubMemberships cm ON ucf.ClubMembershipId = cm.Id
WHERE ucf.ClubFeatureId = 1
AND ucf.IsDeleted = 0
AND (ucf.Notes IS NULL OR ucf.Notes = '')
```
### 3. Hangfire Job Not Running
**راه‌حل**:
1. چک کردن Hangfire Dashboard: `/hangfire`
2. بررسی لاگ‌ها در Seq
3. تأیید ثبت Job در `Program.cs`
### 4. Network Timeout
**علت**: سرور چتیکا در دسترس نیست
**راه‌حل**:
- Retry Policy خودکار 3 بار تلاش می‌کند
- بررسی لاگ‌ها برای خطای دقیق
- تماس با پشتیبانی چتیکا
---
## 📊 Monitoring
### Logs to Watch
```
🚀 Starting Chatika account activation job
📋 Found {Count} users pending Chatika activation
🤖 Creating Chatika account for mobile: 0912***
✅ Chatika account activated for user {UserId}
⚠️ Failed to create Chatika account for user {UserId}: {Error}
❌ Network error calling Chatika API
🏁 Chatika activation job completed. Success: {X}, Failed: {Y}
```
### Seq Query
```
ApplicationName = "CMSMicroservice" AND Message LIKE "%Chatika%"
```
---
## 📚 مستندات مرتبط
- [Club Features System](./club-features-system.md)
- [Hangfire Jobs Guide](./hangfire-jobs.md)
- [Commission System](./commission-system.md)
-490
View File
@@ -1,490 +0,0 @@
# Club Feature Management Services - Implementation Guide
## Overview
Admin services for managing user club features (enable/disable features per user).
## Created Files
### 1. CQRS Layer (Application)
#### Query: GetUserClubFeatures
**Location:** `/CMS/src/CMSMicroservice.Application/ClubFeatureCQ/Queries/GetUserClubFeatures/`
**Files:**
- `GetUserClubFeaturesQuery.cs` - Query definition
- `GetUserClubFeaturesQueryHandler.cs` - Query handler
- `UserClubFeatureDto.cs` - Response DTO
**Purpose:** Get list of all club features for a specific user with their active status.
**Input:**
```csharp
public record GetUserClubFeaturesQuery : IRequest<List<UserClubFeatureDto>>
{
public long UserId { get; init; }
}
```
**Output:**
```csharp
public class UserClubFeatureDto
{
public long Id { get; set; }
public long UserId { get; set; }
public long ClubMembershipId { get; set; }
public long ClubFeatureId { get; set; }
public string FeatureTitle { get; set; }
public string? FeatureDescription { get; set; }
public bool IsActive { get; set; }
public DateTime GrantedAt { get; set; }
public string? Notes { get; set; }
}
```
**Logic:**
- Joins `UserClubFeatures` with `ClubFeature` table
- Filters by `UserId` and `!IsDeleted`
- Returns list of features with their active status
---
#### Command: ToggleUserClubFeature
**Location:** `/CMS/src/CMSMicroservice.Application/ClubFeatureCQ/Commands/ToggleUserClubFeature/`
**Files:**
- `ToggleUserClubFeatureCommand.cs` - Command definition
- `ToggleUserClubFeatureCommandHandler.cs` - Command handler
- `ToggleUserClubFeatureResponse.cs` - Response DTO
**Purpose:** Enable or disable a specific club feature for a user.
**Input:**
```csharp
public record ToggleUserClubFeatureCommand : IRequest<ToggleUserClubFeatureResponse>
{
public long UserId { get; init; }
public long ClubFeatureId { get; init; }
public bool IsActive { get; init; }
}
```
**Output:**
```csharp
public class ToggleUserClubFeatureResponse
{
public bool Success { get; set; }
public string Message { get; set; }
public long? UserClubFeatureId { get; set; }
public bool? IsActive { get; set; }
}
```
**Validations:**
1. ✅ User exists and not deleted
2. ✅ Club feature exists and not deleted
3. ✅ User has this feature assigned (exists in UserClubFeatures)
**Logic:**
- Find `UserClubFeature` record by `UserId` + `ClubFeatureId`
- Update `IsActive` field
- Set `LastModified` timestamp
- Save changes
**Error Messages:**
- "کاربر یافت نشد" - User not found
- "ویژگی باشگاه یافت نشد" - Club feature not found
- "این ویژگی برای کاربر یافت نشد" - User doesn't have this feature
**Success Messages:**
- "ویژگی با موفقیت فعال شد" - Feature activated successfully
- "ویژگی با موفقیت غیرفعال شد" - Feature deactivated successfully
---
### 2. gRPC Layer (Protobuf + WebApi)
#### Proto Definition
**File:** `/CMS/src/CMSMicroservice.Protobuf/Protos/clubmembership.proto`
**Added RPC Methods:**
```protobuf
rpc GetUserClubFeatures(GetUserClubFeaturesRequest) returns (GetUserClubFeaturesResponse){
option (google.api.http) = {
get: "/ClubFeature/GetUserFeatures"
};
};
rpc ToggleUserClubFeature(ToggleUserClubFeatureRequest) returns (ToggleUserClubFeatureResponse){
option (google.api.http) = {
post: "/ClubFeature/ToggleFeature"
body: "*"
};
};
```
**Message Definitions:**
```protobuf
message GetUserClubFeaturesRequest {
int64 user_id = 1;
}
message GetUserClubFeaturesResponse {
repeated UserClubFeatureModel features = 1;
}
message UserClubFeatureModel {
int64 id = 1;
int64 user_id = 2;
int64 club_membership_id = 3;
int64 club_feature_id = 4;
string feature_title = 5;
string feature_description = 6;
bool is_active = 7;
google.protobuf.Timestamp granted_at = 8;
string notes = 9;
}
message ToggleUserClubFeatureRequest {
int64 user_id = 1;
int64 club_feature_id = 2;
bool is_active = 3;
}
message ToggleUserClubFeatureResponse {
bool success = 1;
string message = 2;
google.protobuf.Int64Value user_club_feature_id = 3;
google.protobuf.BoolValue is_active = 4;
}
```
---
#### gRPC Service Implementation
**File:** `/CMS/src/CMSMicroservice.WebApi/Services/ClubMembershipService.cs`
**Added Methods:**
```csharp
public override async Task<GetUserClubFeaturesResponse> GetUserClubFeatures(
GetUserClubFeaturesRequest request,
ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<
GetUserClubFeaturesRequest,
GetUserClubFeaturesQuery,
GetUserClubFeaturesResponse>(request, context);
}
public override async Task<Protobuf.Protos.ClubMembership.ToggleUserClubFeatureResponse>
ToggleUserClubFeature(
ToggleUserClubFeatureRequest request,
ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<
ToggleUserClubFeatureRequest,
ToggleUserClubFeatureCommand,
Protobuf.Protos.ClubMembership.ToggleUserClubFeatureResponse>(request, context);
}
```
---
#### AutoMapper Profile
**File:** `/CMS/src/CMSMicroservice.WebApi/Common/Mappings/ClubFeatureProfile.cs`
**Mappings:**
1. `GetUserClubFeaturesRequest``GetUserClubFeaturesQuery`
2. `UserClubFeatureDto``UserClubFeatureModel` (Proto)
3. `List<UserClubFeatureDto>``GetUserClubFeaturesResponse`
4. `ToggleUserClubFeatureRequest``ToggleUserClubFeatureCommand`
5. `ToggleUserClubFeatureResponse` (App) → `ToggleUserClubFeatureResponse` (Proto)
**Special Handling:**
- DateTime conversion to `Timestamp` (Protobuf format)
- Null-safe mapping for optional fields
- Fully qualified type names to avoid ambiguity
---
## API Endpoints
### 1. Get User Club Features
**Method:** GET
**Endpoint:** `/ClubFeature/GetUserFeatures`
**Request:**
```json
{
"user_id": 123
}
```
**Response:**
```json
{
"features": [
{
"id": 1,
"user_id": 123,
"club_membership_id": 456,
"club_feature_id": 1,
"feature_title": "دسترسی به فروشگاه تخفیف",
"feature_description": "امکان خرید از فروشگاه تخفیف",
"is_active": true,
"granted_at": "2025-12-09T18:30:00Z",
"notes": "اعطا شده به‌طور خودکار هنگام فعالسازی"
}
]
}
```
---
### 2. Toggle User Club Feature
**Method:** POST
**Endpoint:** `/ClubFeature/ToggleFeature`
**Request:**
```json
{
"user_id": 123,
"club_feature_id": 1,
"is_active": false
}
```
**Response (Success):**
```json
{
"success": true,
"message": "ویژگی با موفقیت غیرفعال شد",
"user_club_feature_id": 1,
"is_active": false
}
```
**Response (Error - User Not Found):**
```json
{
"success": false,
"message": "کاربر یافت نشد"
}
```
**Response (Error - Feature Not Found):**
```json
{
"success": false,
"message": "ویژگی باشگاه یافت نشد"
}
```
**Response (Error - User Doesn't Have Feature):**
```json
{
"success": false,
"message": "این ویژگی برای کاربر یافت نشد"
}
```
---
## Database Schema
### Table: UserClubFeatures
Existing table with newly added `IsActive` field:
```sql
CREATE TABLE [CMS].[UserClubFeatures]
(
[Id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[UserId] BIGINT NOT NULL,
[ClubMembershipId] BIGINT NOT NULL,
[ClubFeatureId] BIGINT NOT NULL,
[GrantedAt] DATETIME2 NOT NULL,
[IsActive] BIT NOT NULL DEFAULT 1, -- ← NEW FIELD
[Notes] NVARCHAR(MAX) NULL,
[Created] DATETIME2 NOT NULL,
[CreatedBy] NVARCHAR(MAX) NULL,
[LastModified] DATETIME2 NULL,
[LastModifiedBy] NVARCHAR(MAX) NULL,
[IsDeleted] BIT NOT NULL DEFAULT 0,
CONSTRAINT FK_UserClubFeatures_Users FOREIGN KEY ([UserId])
REFERENCES [Identity].[Users]([Id]),
CONSTRAINT FK_UserClubFeatures_ClubMembership FOREIGN KEY ([ClubMembershipId])
REFERENCES [CMS].[ClubMembership]([Id]),
CONSTRAINT FK_UserClubFeatures_ClubFeatures FOREIGN KEY ([ClubFeatureId])
REFERENCES [CMS].[ClubFeatures]([Id])
);
```
---
## Usage Examples
### Admin Panel Scenario
#### 1. View User's Club Features
```csharp
// Admin selects user ID: 123
var request = new GetUserClubFeaturesRequest { UserId = 123 };
var response = await client.GetUserClubFeaturesAsync(request);
// Display in grid:
foreach (var feature in response.Features)
{
Console.WriteLine($"Feature: {feature.FeatureTitle}");
Console.WriteLine($"Status: {(feature.IsActive ? "فعال" : "غیرفعال")}");
Console.WriteLine($"Granted: {feature.GrantedAt}");
Console.WriteLine("---");
}
```
**Output:**
```
Feature: دسترسی به فروشگاه تخفیف
Status: فعال
Granted: 2025-12-09 18:30:00
---
Feature: دسترسی به کمیسیون هفتگی
Status: فعال
Granted: 2025-12-09 18:30:00
---
Feature: دسترسی به شارژ شبکه
Status: غیرفعال
Granted: 2025-12-09 18:30:00
---
```
---
#### 2. Disable a Feature
```csharp
// Admin clicks "Disable" on Feature ID: 3
var request = new ToggleUserClubFeatureRequest
{
UserId = 123,
ClubFeatureId = 3,
IsActive = false
};
var response = await client.ToggleUserClubFeatureAsync(request);
if (response.Success)
{
Console.WriteLine(response.Message);
// Output: ویژگی با موفقیت غیرفعال شد
}
```
---
#### 3. Re-enable a Feature
```csharp
// Admin clicks "Enable" on Feature ID: 3
var request = new ToggleUserClubFeatureRequest
{
UserId = 123,
ClubFeatureId = 3,
IsActive = true
};
var response = await client.ToggleUserClubFeatureAsync(request);
if (response.Success)
{
Console.WriteLine(response.Message);
// Output: ویژگی با موفقیت فعال شد
}
```
---
## Testing Checklist
### Unit Tests (Recommended)
- [ ] GetUserClubFeaturesQueryHandler returns correct DTOs
- [ ] ToggleUserClubFeatureCommandHandler validates user exists
- [ ] ToggleUserClubFeatureCommandHandler validates feature exists
- [ ] ToggleUserClubFeatureCommandHandler validates user has feature
- [ ] ToggleUserClubFeatureCommandHandler updates IsActive correctly
- [ ] ToggleUserClubFeatureCommandHandler sets LastModified timestamp
### Integration Tests
- [ ] gRPC GetUserClubFeatures endpoint returns data
- [ ] gRPC ToggleUserClubFeature endpoint updates database
- [ ] AutoMapper mappings work correctly
- [ ] Proto serialization/deserialization works
### Manual Testing
1. **Get Features:**
```bash
grpcurl -d '{"user_id": 123}' \
-plaintext localhost:5000 \
clubmembership.ClubMembershipContract/GetUserClubFeatures
```
2. **Disable Feature:**
```bash
grpcurl -d '{"user_id": 123, "club_feature_id": 1, "is_active": false}' \
-plaintext localhost:5000 \
clubmembership.ClubMembershipContract/ToggleUserClubFeature
```
3. **Verify in Database:**
```sql
SELECT Id, UserId, ClubFeatureId, IsActive, LastModified
FROM CMS.UserClubFeatures
WHERE UserId = 123;
```
---
## Build Status
✅ **All projects build successfully**
- CMSMicroservice.Domain: ✅
- CMSMicroservice.Application: ✅ (0 errors, 274 warnings)
- CMSMicroservice.Protobuf: ✅
- CMSMicroservice.WebApi: ✅ (0 errors, 17 warnings)
---
## Next Steps (Optional Enhancements)
1. **Authorization:**
- Add `[Authorize(Roles = "Admin")]` attribute
- Validate admin permissions before toggling
2. **Audit Logging:**
- Log who changed the feature status
- Track `LastModifiedBy` field
3. **Bulk Operations:**
- Add endpoint to toggle multiple features at once
- Add endpoint to enable/disable all features for a user
4. **History Tracking:**
- Create `UserClubFeatureHistory` table
- Log every status change with timestamp and reason
5. **Notifications:**
- Send notification to user when feature is disabled
- Email/SMS alert for important features
6. **Business Rules:**
- Add validation: prevent disabling critical features
- Add expiration dates for features
- Add feature dependencies (e.g., Feature B requires Feature A)
---
## Summary
✅ Created CQRS Query + Command for club feature management
✅ Created gRPC Proto definitions and services
✅ Created AutoMapper mappings
✅ All builds successful
✅ Ready for deployment and testing
**Total Files Created:** 8
**Total Lines of Code:** ~350
**Build Errors:** 0
**Status:** ✅ Complete and ready for use
-191
View File
@@ -1,191 +0,0 @@
# راهنمای پیکربندی Email و SMS
## قالب‌های پیامک (SmsTemplates)
> **فایل**: `CMSMicroservice.Domain/Common/SmsTemplates.cs`
همه قالب‌های پیامک در یک کلاس متمرکز شده‌اند:
```csharp
public static class SmsTemplates
{
// وام دایا
public static string DayaLoanReceived(string? firstName, long amount)
=> $"{GetUserName(firstName)} عزیز، مبلغ {amount:N0} ریال وام دایا به کیف پول شما واریز شد. کارابازار";
// فعال‌سازی باشگاه
public static string ClubActivated(string? firstName)
=> $"{GetUserName(firstName)} عزیز، حساب باشگاه شما فعال شد. کارابازار";
// خرید پکیج
public static string PackagePurchased(string? firstName, string packageName)
=> $"{GetUserName(firstName)} عزیز، پکیج {packageName} با موفقیت خریداری شد. کارابازار";
// واریز کمیسیون
public static string CommissionDeposited(string? firstName, long amount)
=> $"{GetUserName(firstName)} عزیز، مبلغ {amount:N0} ریال کمیسیون به کیف پول شما واریز شد. کارابازار";
// برداشت موفق
public static string WithdrawalSuccess(string? firstName, long amount)
=> $"{GetUserName(firstName)} عزیز، درخواست برداشت {amount:N0} ریال با موفقیت انجام شد. کارابازار";
// پیوستن به شبکه
public static string NetworkJoined(string? firstName, string referrerName)
=> $"{GetUserName(firstName)} عزیز، به شبکه {referrerName} پیوستید. کارابازار";
// زیرمجموعه جدید
public static string NewDownline(string? firstName, string newMemberName)
=> $"{GetUserName(firstName)} عزیز، {newMemberName} به زیرمجموعه شما اضافه شد. کارابازار";
// کد OTP
public static string OtpCode(string code)
=> $"کد تأیید شما: {code}\nکارابازار";
// خوش‌آمدگویی
public static string Welcome(string? firstName)
=> $"{GetUserName(firstName)} عزیز، به کارابازار خوش آمدید!";
}
```
### نحوه استفاده:
```csharp
// تزریق سرویس
private readonly IKavenegarService _smsService;
// ارسال پیامک
var message = SmsTemplates.DayaLoanReceived(user.FirstName, 56_000_000);
await _smsService.SendAsync(user.PhoneNumber, message);
```
---
## تنظیمات Email (Gmail)
### مرحله 1: ایجاد App Password در Gmail
1. به [Google Account Security](https://myaccount.google.com/security) بروید
2. گزینه "2-Step Verification" را فعال کنید
3. به بخش "App passwords" بروید
4. یک App Password جدید با نام "FourSat CMS" ایجاد کنید
5. پسورد 16 رقمی را در `appsettings.Production.json` در فیلد `SmtpPassword` قرار دهید
### مرحله 2: تنظیم appsettings.Production.json
```json
"Email": {
"Enabled": true,
"SmtpHost": "smtp.gmail.com",
"SmtpPort": 587,
"SmtpUsername": "your-email@gmail.com", // ایمیل Gmail خود
"SmtpPassword": "your-16-digit-app-password", // App Password از مرحله 1
"FromEmail": "noreply@foursat.com", // ایمیل فرستنده (می‌تواند همان Gmail باشد)
"FromName": "FourSat CMS",
"EnableSsl": true
}
```
### سایر سرویس‌های SMTP:
#### Outlook/Microsoft 365:
```json
"SmtpHost": "smtp.office365.com",
"SmtpPort": 587
```
#### Yahoo Mail:
```json
"SmtpHost": "smtp.mail.yahoo.com",
"SmtpPort": 587
```
---
## تنظیمات SMS (کاوه نگار)
### مرحله 1: ثبت‌نام در کاوه نگار
1. به [Kavenegar.com](https://panel.kavenegar.com/client/membership/register) بروید
2. ثبت‌نام کنید و حساب خود را تأیید کنید
3. از پنل، API Key خود را کپی کنید
### مرحله 2: تنظیم appsettings.Production.json
```json
"Sms": {
"Enabled": true,
"Provider": "Kavenegar",
"KavenegarApiKey": "YOUR_KAVENEGAR_API_KEY", // API Key از پنل کاوه نگار
"Sender": "10008663" // شماره ارسال‌کننده (از پنل کاوه نگار)
}
```
### نکات مهم:
- شماره `Sender` باید از پنل کاوه نگار تهیه شود
- برای تست می‌توانید از شماره‌های رایگان استفاده کنید
- هزینه هر پیامک بسته به نوع خط متفاوت است
---
## تست کردن
### تست Email:
```bash
# در محیط Development
curl -X POST "http://localhost:5133/api/admin/trigger-weekly-calculation"
```
### تست SMS:
همان دستور بالا را اجرا کنید. سیستم به صورت خودکار:
- Email ارسال می‌کند (اگر User.Email پر باشد)
- SMS ارسال می‌کند (اگر User.Mobile پر باشد)
### بررسی Log ها:
```bash
# در ترمینال سرویس CMS
# پیام‌های زیر را مشاهده کنید:
# 📧 Email sent to {Email}: {Subject}
# 📱 SMS sent to {PhoneNumber}: {MessageId}
```
---
## امنیت
### ⚠️ مهم:
1. فایل `appsettings.Production.json` را به Git اضافه نکنید
2. از Environment Variables یا Azure Key Vault استفاده کنید
3. API Key ها را هرگز در کد سورس قرار ندهید
### استفاده از Environment Variables:
```bash
# Linux/Mac
export Email__SmtpPassword="your-app-password"
export Sms__KavenegarApiKey="your-api-key"
# Windows
set Email__SmtpPassword=your-app-password
set Sms__KavenegarApiKey=your-api-key
```
---
## خطایابی (Troubleshooting)
### Email ارسال نمی‌شود:
1. App Password را صحیح وارد کرده‌اید؟
2. 2-Step Verification در Gmail فعال است؟
3. Port 587 باز است؟
4. `EnableSsl: true` تنظیم شده؟
### SMS ارسال نمی‌شود:
1. API Key صحیح است؟
2. اعتبار حساب کاوه نگار کافی است؟
3. شماره `Sender` معتبر است؟
4. فرمت شماره موبایل صحیح است؟ (09xxxxxxxxx)
### Log ها را بررسی کنید:
```bash
tail -f /tmp/cms_run.log
```
-410
View File
@@ -1,410 +0,0 @@
# Payment Architecture with PYMS Microservice
**تاریخ**: 2024-12-02
**وضعیت**: Architecture Document
**اولویت**: 🔴 بالا (اطلاعات مهم برای Phase 9)
---
## 📋 خلاصه
**درگاه پرداخت** در این پروژه از طریق **مایکروسرویس PYMS** (`Afrino.PYMSMicroservice.Protobuf`) مدیریت می‌شود.
**CMS Microservice** فقط **نتیجه نهایی پرداخت** را ثبت می‌کند و خودش درگاه پرداخت را پیاده‌سازی نمی‌کند.
---
## 🏗️ معماری کلی
```
[User Frontend]
[FrontOffice.BFF] ← درخواست خرید از اینجا شروع می‌شود
[PYMS Microservice] ← مدیریت درگاه پرداخت (Afrino.PYMSMicroservice.Protobuf)
[Payment Gateway: در PYMS/Gateway - نه CMS]
↓ (Callback)
[PYMS Microservice] ← تایید پرداخت
[CMS Microservice] ← **فقط ثبت نتیجه** (Transaction با RefId)
```
### توضیح جریان:
1. **کاربر** محصول را در Frontend انتخاب می‌کند
2. **FrontOffice.BFF** درخواست خرید را به **PYMS Microservice** می‌فرستد
3. **PYMS/Gateway** با درگاه پرداخت (بانک) ارتباط برقرار می‌کند و پرداخت را انجام می‌دهد
4. **Gateway** نتیجه پرداخت را به **CMS Callback** می‌فرستد
5. **CMS** تراکنش را تایید و عملیات بعدی (فعال‌سازی، اضافه PV، Wallet) را انجام می‌دهد
4. **PYMS** URL درگاه را برمی‌گرداند
5. کاربر به درگاه ریدایرکت می‌شود و پرداخت می‌کند
6. بعد از پرداخت، **Callback** به **PYMS** برمی‌گردد
7. **PYMS** پرداخت را Verify می‌کند
8. **FrontOffice.BFF** نتیجه را به **CMS** می‌فرستد
9. **CMS** Transaction را با RefId و وضعیت نهایی ثبت می‌کند
---
## 📦 Package: `Afrino.PYMSMicroservice.Protobuf`
**Version**: 0.0.11
**Type**: gRPC Protobuf Client
**Namespace**: `PYMSMicroservice.Protobuf.Protos.Transaction`
### Dependencies:
- Google.Protobuf (3.23.3)
- Grpc.Core.Api (2.54.0)
- FluentValidation (11.2.2)
- Google.Api.CommonProtos (2.10.0)
---
## 🔧 TransactionContract Service
### Client Class:
```csharp
using PYMSMicroservice.Protobuf.Protos.Transaction;
using Grpc.Core;
var client = new TransactionContract.TransactionContractClient(channel);
```
### Available Methods:
#### 1. **PaymentRequest** (شروع پرداخت)
```csharp
// Request
var request = new PaymentRequestRequest
{
MerchantId = "YOUR_MERCHANT_ID", // شناسه فروشنده
Amount = 100000, // مبلغ به ریال (یا تومان - بستگی به Currency)
CallbackUrl = "https://yoursite.com/payment/callback",
Description = "خرید بسته طلایی",
Mobile = "09123456789", // اختیاری
Email = "user@example.com", // اختیاری
Currency = CurrencyEnum.Irt, // IRR (ریال) یا IRT (تومان)
Type = TransactionTypeEnum.Real, // Real یا Sandbox
OrderId = "ORDER_123456" // اختیاری - شناسه سفارش خودمان
};
// Call
var response = await client.PaymentRequestAsync(request);
// Response
Console.WriteLine(response.PaymentGWUrl);
// مثال: "https://bpm.shaparak.ir/pgwchannel/startpay.mellat?RefId=123456"
```
**Response Fields**:
- `PaymentGWUrl` (string): URL درگاه پرداخت که کاربر باید به آن ریدایرکت شود
#### 2. **PaymentVerification** (تایید پرداخت)
```csharp
// Request
var request = new PaymentVerificationRequest
{
Authority = "AUTHORITY_FROM_CALLBACK", // Authority که از callback می‌آید
Status = "OK" // Status که از callback می‌آید (OK/NOK)
};
// Call
var response = await client.PaymentVerificationAsync(request);
// Response
if (response.PaymentStatus)
{
Console.WriteLine($"پرداخت موفق!");
Console.WriteLine($"RefId: {response.RefId}");
Console.WriteLine($"OrderId: {response.OrderId}");
Console.WriteLine($"Message: {response.Message}");
Console.WriteLine($"VerificationStatusCode: {response.VerificationStatusCode}");
}
else
{
Console.WriteLine($"پرداخت ناموفق: {response.Message}");
}
```
**Response Fields**:
- `Id` (long): شناسه تراکنش در سیستم PYMS
- `PaymentStatus` (bool): وضعیت پرداخت (true = موفق، false = ناموفق)
- `Message` (string): پیام وضعیت
- `RefId` (string): شناسه مرجع از درگاه پرداخت
- `OrderId` (string): شناسه سفارش که در PaymentRequest ارسال شده
- `VerificationStatusCode` (int): کد وضعیت تایید
#### 3. **CreateNewTransaction** (ثبت تراکنش جدید)
```csharp
var request = new CreateNewTransactionRequest
{
MerchantId = "...",
Amount = 100000,
CallbackUrl = "...",
Description = "...",
Currency = CurrencyEnum.Irt,
PaymentStatus = false, // false در ابتدا
Type = TransactionTypeEnum.Real
};
var response = await client.CreateNewTransactionAsync(request);
Console.WriteLine($"Transaction Id: {response.Id}");
```
#### 4. **UpdateTransaction** (به‌روزرسانی تراکنش)
```csharp
var request = new UpdateTransactionRequest
{
Id = transactionId,
PaymentStatus = true, // بعد از verify
RefId = "...",
VerificationStatusCode = 100,
VerificationStatusMessage = "تراکنش موفق"
};
await client.UpdateTransactionAsync(request);
```
#### 5. **GetTransaction** (دریافت تراکنش)
```csharp
var request = new GetTransactionRequest
{
Id = transactionId,
// یا
Authority = "AUTHORITY_FROM_CALLBACK"
};
var response = await client.GetTransactionAsync(request);
```
#### 6. **GetAllTransactionByFilter** (لیست تراکنش‌ها)
```csharp
var request = new GetAllTransactionByFilterRequest
{
PaginationState = new PaginationState { PageNumber = 1, PageSize = 10 },
Filter = new GetAllTransactionByFilterFilter
{
MerchantId = "...",
PaymentStatus = true
}
};
var response = await client.GetAllTransactionByFilterAsync(request);
// response.Models: لیست تراکنش‌ها
// response.MetaData: اطلاعات صفحه‌بندی
```
---
## 🔑 Enums
### CurrencyEnum
```csharp
public enum CurrencyEnum
{
Irr = 0, // ریال
Irt = 1 // تومان
}
```
### TransactionTypeEnum
```csharp
public enum TransactionTypeEnum
{
Real = 0, // تراکنش واقعی
Sandbox = 1 // تراکنش تستی
}
```
---
## 💡 نکات مهم برای CMS
### 1. **CMS فقط نتیجه را ثبت می‌کند**
CMS نباید خودش با درگاه پرداخت ارتباط برقرار کند. این کار توسط **PYMS Microservice** انجام می‌شود.
### 2. **Flow پیشنهادی برای Phase 9 (Club Discount Shop)**:
#### در FrontOffice.BFF:
```csharp
// 1. کاربر محصول را انتخاب می‌کند
var product = await cmsClient.GetProductAsync(productId);
// 2. محاسبه تخفیف
var userWallet = await cmsClient.GetUserWalletAsync(userId);
var maxDiscountAmount = product.Price * (product.MaxDiscountPercent / 100);
var actualDiscountAmount = Math.Min(userWallet.DiscountBalance, maxDiscountAmount);
var gatewayAmount = product.Price - actualDiscountAmount;
// 3. ثبت Order در CMS با وضعیت Pending
var order = await cmsClient.CreateDiscountOrderAsync(new CreateDiscountOrderRequest
{
UserId = userId,
ProductId = productId,
TotalAmount = product.Price,
DiscountAmount = actualDiscountAmount,
GatewayAmount = gatewayAmount,
Status = OrderStatus.Pending
});
// 4. درخواست پرداخت از PYMS
var paymentResponse = await pymsClient.PaymentRequestAsync(new PaymentRequestRequest
{
MerchantId = "YOUR_MERCHANT_ID",
Amount = (long)gatewayAmount, // مبلغی که باید از درگاه پرداخت شود
CallbackUrl = $"https://yoursite.com/payment/verify?orderId={order.Id}",
Description = $"خرید {product.Title}",
Currency = CurrencyEnum.Irt,
Type = TransactionTypeEnum.Real,
OrderId = order.Id.ToString()
});
// 5. ریدایرکت به درگاه
return Redirect(paymentResponse.PaymentGWUrl);
```
#### در Callback (بعد از بازگشت از درگاه):
```csharp
// 1. دریافت Authority و Status از Query String
var authority = Request.Query["Authority"];
var status = Request.Query["Status"];
var orderId = Request.Query["orderId"];
// 2. تایید پرداخت از PYMS
var verifyResponse = await pymsClient.PaymentVerificationAsync(new PaymentVerificationRequest
{
Authority = authority,
Status = status
});
// 3. ثبت نتیجه در CMS
if (verifyResponse.PaymentStatus)
{
// 3.1. کسر DiscountBalance
await cmsClient.DeductDiscountBalanceAsync(new DeductDiscountBalanceRequest
{
UserId = userId,
Amount = order.DiscountAmount,
Description = $"خرید محصول {product.Title}",
RefId = verifyResponse.RefId
});
// 3.2. ثبت Transaction در CMS
await cmsClient.CreateTransactionAsync(new CreateTransactionRequest
{
UserId = userId,
Type = TransactionType.DiscountPurchase,
Amount = order.TotalAmount,
DiscountAmount = order.DiscountAmount,
GatewayAmount = order.GatewayAmount,
RefId = verifyResponse.RefId,
Status = TransactionStatus.Completed,
Description = $"خرید {product.Title}"
});
// 3.3. تغییر وضعیت Order به Completed
await cmsClient.CompleteDiscountOrderAsync(new CompleteDiscountOrderRequest
{
OrderId = orderId,
RefId = verifyResponse.RefId
});
return View("PaymentSuccess");
}
else
{
// 3.4. تغییر وضعیت Order به Failed
await cmsClient.FailDiscountOrderAsync(new FailDiscountOrderRequest
{
OrderId = orderId,
ErrorMessage = verifyResponse.Message
});
return View("PaymentFailed", verifyResponse.Message);
}
```
### 3. **Entity های مورد نیاز در CMS**:
```csharp
// Domain/Entities/DiscountOrder.cs
public class DiscountOrder
{
public long Id { get; set; }
public long UserId { get; set; }
public long ProductId { get; set; }
public decimal TotalAmount { get; set; }
public decimal DiscountAmount { get; set; } // مبلغ از DiscountBalance
public decimal GatewayAmount { get; set; } // مبلغ از درگاه
public OrderStatus Status { get; set; } // Pending/Completed/Failed
public string? RefId { get; set; } // RefId از PYMS
public string? ErrorMessage { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? CompletedAt { get; set; }
// Navigation
public User User { get; set; }
public Product Product { get; set; }
}
// Domain/Enums/OrderStatus.cs
public enum OrderStatus
{
Pending = 0, // در انتظار پرداخت
Completed = 1, // پرداخت موفق
Failed = 2 // پرداخت ناموفق
}
```
### 4. **Commands مورد نیاز در CMS**:
- `CreateDiscountOrderCommand`: ثبت سفارش اولیه
- `CompleteDiscountOrderCommand`: تکمیل سفارش بعد از پرداخت موفق
- `FailDiscountOrderCommand`: شکست سفارش
- `DeductDiscountBalanceCommand`: کسر از DiscountBalance
---
## ✅ مزایای این معماری
1.**Separation of Concerns**: CMS فقط روی business logic خودش تمرکز دارد
2.**Single Responsibility**: PYMS مسئول پرداخت است، CMS مسئول ثبت نتیجه
3.**Easy Testing**: می‌توان PYMS را با Mock جایگزین کرد
4.**Scalability**: هر microservice به‌صورت مستقل scale می‌شود
5.**Maintainability**: تغییرات در درگاه پرداخت فقط در PYMS انجام می‌شود
---
## ⚠️ نکات امنیتی
1. **همیشه Verify کنید**: حتی اگر Status=OK باشد، حتماً PaymentVerification را صدا بزنید
2. **Callback را Validate کنید**: مطمئن شوید request واقعاً از درگاه آمده (IP whitelisting)
3. **OrderId را Validate کنید**: مطمئن شوید OrderId متعلق به همان کاربری است که لاگین کرده
4. **مبلغ را چک کنید**: مبلغ پرداخت شده با مبلغ سفارش مطابقت داشته باشد
5. **Idempotency**: از ثبت تکراری تراکنش جلوگیری کنید (با RefId)
---
## 📚 مثال کامل برای Phase 9
در فاز 9، باید:
1.**FrontOffice.BFF** درخواست پرداخت را به **PYMS** بفرستد
2.**PYMS** URL درگاه را برگرداند
3. ✅ بعد از بازگشت، **FrontOffice.BFF** verify کند
4. ✅ نتیجه را به **CMS** بفرستد تا:
- DiscountBalance کسر شود
- Transaction ثبت شود
- Order تکمیل شود
---
**نتیجه‌گیری**:
-**Payment Gateway Service** (فقط DayaPaymentService برای Payout) **فقط برای پرداخت به کاربران است**
-**Transaction System در CMS** برای دریافت نتیجه پرداخت از Gateway و ادامه عملیات:
- Entity: `Transaction` (ReferenceId, Amount, Status, Gateway)
- Commands: `CreateTransaction`, `VerifyTransaction` (Callback), `RefundTransaction`
- Queries: `GetTransactions`, `GetUserTransactions`
- جریان: User → Gateway (پرداخت) → Callback به CMS → CMS (فعال‌سازی)
- ✅ این سرویس‌ها فقط برای **مستندسازی** و **درک معماری** نوشته شدند
- ✅ در عمل، **PYMS Microservice** مسئول ارتباط با درگاه است
-**CMS فقط نتیجه را ثبت می‌کند**
File diff suppressed because it is too large Load Diff
-120
View File
@@ -1,120 +0,0 @@
# 🔧 SystemConstants - مقادیر ثابت سیستم
> **فایل**: `CMSMicroservice.Domain/Common/SystemConstants.cs`
> **آخرین بروزرسانی**: ۷ دی ۱۴۰۴
---
## 📋 هدف
این کلاس شامل تمام مقادیر ثابت سیستم است که در چندین جای مختلف استفاده می‌شوند.
به جای hardcode کردن اعداد در کد، از این ثابت‌ها استفاده کنید.
---
## 📊 مقادیر موجود
### Club Configuration
| ثابت | مقدار | توضیح |
|------|-------|-------|
| `ClubJoiningPercentage` | 0.35 (35%) | درصد کمیسیون پیوستن به باشگاه |
| `ClubActivationThreshold` | 0.5 (50%) | آستانه فعال‌سازی باشگاه |
### Commission Configuration
| ثابت | مقدار | توضیح |
|------|-------|-------|
| `MaxCalculationAttempts` | 3 | حداکثر تلاش برای محاسبه کمیسیون |
| `DefaultCommissionPoolDays` | 7 | تعداد روزهای استخر کمیسیون |
### Package Amounts
| ثابت | مقدار | توضیح |
|------|-------|-------|
| `GoldenPackageAmount` | 56,000,000 | مبلغ پکیج طلایی (56 میلیون ریال) |
| `DayaLoanAmount` | 56,000,000 | مبلغ وام دایا (56 میلیون ریال) |
---
## 💻 کد
```csharp
namespace CMSMicroservice.Domain.Common;
/// <summary>
/// مقادیر ثابت سیستم که در چند جای مختلف استفاده می‌شوند
/// </summary>
public static class SystemConstants
{
// Club Configuration
public const decimal ClubJoiningPercentage = 0.35m; // 35% کمیسیون پیوستن به باشگاه
public const decimal ClubActivationThreshold = 0.5m; // 50% آستانه فعال‌سازی
// Commission Configuration
public const int MaxCalculationAttempts = 3; // حداکثر تلاش محاسبه
public const int DefaultCommissionPoolDays = 7; // روزهای استخر کمیسیون
// Package Amounts
public const long GoldenPackageAmount = 56_000_000; // 56 میلیون - پکیج طلایی
public const long DayaLoanAmount = 56_000_000; // 56 میلیون - وام دایا
}
```
---
## 🔍 نحوه استفاده
### در Handler ها:
```csharp
using CMSMicroservice.Domain.Common;
public class ProcessDayaLoanApprovalCommandHandler
{
public async Task<Unit> Handle(...)
{
// به جای: var amount = 56_000_000;
var amount = SystemConstants.DayaLoanAmount;
await DepositToWallet(userId, amount);
}
}
```
### در Validation ها:
```csharp
public class ValidateGoldenPackagePurchaseQueryHandler
{
public async Task<bool> Handle(...)
{
var requiredAmount = SystemConstants.GoldenPackageAmount;
return user.WalletBalance >= requiredAmount;
}
}
```
---
## ⚠️ قوانین
1. **همیشه از ثابت‌ها استفاده کنید** - هرگز مقادیر magic number در کد ننویسید
2. **تغییر مقادیر** - برای تغییر یک مقدار، فقط این فایل را تغییر دهید
3. **ثابت‌های جدید** - اگر مقداری در بیش از یک جا استفاده می‌شود، به این فایل اضافه کنید
4. **نام‌گذاری** - از نام‌های توصیفی استفاده کنید (مثلاً `GoldenPackageAmount` نه `Amount1`)
---
## 📁 فایل‌های مرتبط
- `SmsTemplates.cs` - قالب‌های پیامک
- `ProcessDayaLoanApprovalCommandHandler.cs` - استفاده از DayaLoanAmount
- `ValidateGoldenPackagePurchaseQueryHandler.cs` - استفاده از GoldenPackageAmount
---
## 🔗 Related Docs
- [email-sms-configuration.md](email-sms-configuration.md) - تنظیمات SMS و قالب‌ها
- [CHANGELOG-2025-12-27.md](../../CHANGELOG-2025-12-27.md) - تاریخچه تغییرات