feat: Add monitoring alerts skeleton and enhance worker with notifications
This commit is contained in:
281
docs/binary-tree-registration-guide.md
Normal file
281
docs/binary-tree-registration-guide.md
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# 🌳 Binary Tree Network Registration Guide
|
||||||
|
|
||||||
|
## 📋 Overview
|
||||||
|
|
||||||
|
از این پس، هر کاربر جدید که در سیستم ثبت میشود، **همزمان** در دو ساختار قرار میگیرد:
|
||||||
|
|
||||||
|
1. **Old System**: `User.ParentId` (برای Backward Compatibility)
|
||||||
|
2. **New Binary Tree System**: `User.NetworkParentId` + `User.LegPosition` (Left/Right)
|
||||||
|
|
||||||
|
این تغییر تضمین میکند که:
|
||||||
|
- ✅ کاربران جدید بلافاصله در محاسبات Commission شرکت میکنند
|
||||||
|
- ✅ نیازی به Migration اضافی نیست
|
||||||
|
- ✅ Binary Tree Constraint رعایت میشود (حداکثر 2 فرزند)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Changes in Registration Flow
|
||||||
|
|
||||||
|
### قبل از تغییر:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var entity = request.Adapt<User>();
|
||||||
|
entity.ReferralCode = UtilExtensions.Generate(digits: 10);
|
||||||
|
await _context.Users.AddAsync(entity, cancellationToken);
|
||||||
|
```
|
||||||
|
|
||||||
|
**مشکل**: فقط `ParentId` Set میشد، `NetworkParentId` و `LegPosition` خالی میماند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### بعد از تغییر:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var entity = request.Adapt<User>();
|
||||||
|
entity.ReferralCode = UtilExtensions.Generate(digits: 10);
|
||||||
|
|
||||||
|
// === محاسبه موقعیت در Binary Tree ===
|
||||||
|
if (request.ParentId.HasValue)
|
||||||
|
{
|
||||||
|
var legPosition = await _networkPlacementService.CalculateLegPositionAsync(
|
||||||
|
request.ParentId.Value, cancellationToken);
|
||||||
|
|
||||||
|
if (legPosition.HasValue)
|
||||||
|
{
|
||||||
|
entity.NetworkParentId = request.ParentId.Value;
|
||||||
|
entity.LegPosition = legPosition.Value; // Left یا Right
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Parent پر است! Auto-Placement یا Error
|
||||||
|
var availableParent = await _networkPlacementService.FindAvailableParentAsync(
|
||||||
|
request.ParentId.Value, cancellationToken);
|
||||||
|
|
||||||
|
// ... Set کردن NetworkParentId و LegPosition با Parent جدید
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.Users.AddAsync(entity, cancellationToken);
|
||||||
|
```
|
||||||
|
|
||||||
|
**مزایا**:
|
||||||
|
- ✅ `NetworkParentId` و `LegPosition` به صورت خودکار محاسبه میشود
|
||||||
|
- ✅ Binary Tree Constraint چک میشود
|
||||||
|
- ✅ اگر Parent پر باشد، Auto-Placement انجام میشود
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📐 Binary Tree Logic
|
||||||
|
|
||||||
|
### قوانین:
|
||||||
|
1. هر Parent فقط **2 فرزند** میتواند داشته باشد (Left & Right)
|
||||||
|
2. فرزند اول: `LegPosition = Left`
|
||||||
|
3. فرزند دوم: `LegPosition = Right`
|
||||||
|
4. اگر Parent پر باشد، سیستم به صورت BFS دنبال Parent خالی میگردد
|
||||||
|
|
||||||
|
### مثال:
|
||||||
|
|
||||||
|
```
|
||||||
|
User1 (Root)
|
||||||
|
/ \
|
||||||
|
User2 (L) User3 (R)
|
||||||
|
/ \
|
||||||
|
User4(L) User5(R)
|
||||||
|
```
|
||||||
|
|
||||||
|
- User2 → Parent=User1, Leg=Left
|
||||||
|
- User3 → Parent=User1, Leg=Right
|
||||||
|
- User4 → Parent=User2, Leg=Left
|
||||||
|
- User5 → Parent=User2, Leg=Right
|
||||||
|
|
||||||
|
اگر کاربر جدید با `ParentId=User1` بیاید:
|
||||||
|
- User1 پر است! (دو فرزند دارد)
|
||||||
|
- سیستم به User2 میرود (BFS)
|
||||||
|
- User2 هم پر است!
|
||||||
|
- به User3 میرود → User3 خالی است
|
||||||
|
- کاربر جدید → Parent=User3, Leg=Left
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ NetworkPlacementService API
|
||||||
|
|
||||||
|
### 1. CalculateLegPositionAsync
|
||||||
|
محاسبه موقعیت (Left/Right) برای کاربر جدید زیر یک Parent مشخص.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var legPosition = await _networkPlacementService.CalculateLegPositionAsync(parentId);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Return Values**:
|
||||||
|
- `NetworkLeg.Left`: اگر Parent فرزند چپ ندارد
|
||||||
|
- `NetworkLeg.Right`: اگر Parent فرزند راست ندارد
|
||||||
|
- `null`: اگر Parent پر است (دو فرزند دارد)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. CanAcceptChildAsync
|
||||||
|
بررسی اینکه آیا Parent میتواند فرزند جدید بپذیرد.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
bool canAccept = await _networkPlacementService.CanAcceptChildAsync(parentId);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Return Values**:
|
||||||
|
- `true`: اگر Parent کمتر از 2 فرزند دارد
|
||||||
|
- `false`: اگر Parent پر است
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. FindAvailableParentAsync (Auto-Placement)
|
||||||
|
پیدا کردن اولین Parent خالی در Binary Tree با استفاده از BFS.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
long? availableParentId = await _networkPlacementService.FindAvailableParentAsync(rootParentId);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Case**:
|
||||||
|
- زمانی که Parent مورد نظر پر است
|
||||||
|
- سیستم به صورت خودکار Parent جایگزین پیدا میکند
|
||||||
|
- از BFS استفاده میکند (Level-by-Level)
|
||||||
|
|
||||||
|
**Return Values**:
|
||||||
|
- `long`: شناسه Parent مناسب
|
||||||
|
- `null`: اگر هیچ Parent خالی پیدا نشد (تمام Binary Tree پر است!)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Error Handling
|
||||||
|
|
||||||
|
### Scenario 1: Parent پر است و Auto-Placement موفق
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Parent اصلی پر است
|
||||||
|
// سیستم Parent جدید پیدا میکند
|
||||||
|
_logger.LogWarning("Parent {ParentId} is full. Auto-placing under {NewParentId}");
|
||||||
|
```
|
||||||
|
|
||||||
|
**نتیجه**: کاربر با موفقیت در جای دیگری قرار میگیرد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenario 2: کل Binary Tree پر است
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"شبکه Parent با شناسه {parentId} پر است و نمیتواند کاربر جدید بپذیرد.");
|
||||||
|
```
|
||||||
|
|
||||||
|
**نتیجه**: Exception پرتاب میشود، ثبت کاربر انجام نمیشود.
|
||||||
|
|
||||||
|
**راه حل**:
|
||||||
|
- افزایش سطح Binary Tree
|
||||||
|
- یا تخصیص دستی Parent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenario 3: Parent وجود ندارد
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var parentExists = await _context.Users.AnyAsync(u => u.Id == parentId);
|
||||||
|
if (!parentExists)
|
||||||
|
{
|
||||||
|
return null; // Parent نامعتبر
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**نتیجه**: `null` برگردانده میشود، Exception پرتاب میشود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Logging & Monitoring
|
||||||
|
|
||||||
|
سیستم Log های زیر را مینویسد:
|
||||||
|
|
||||||
|
### Success:
|
||||||
|
```
|
||||||
|
User 123 placed in Binary Tree: Parent=45, Leg=Left
|
||||||
|
```
|
||||||
|
|
||||||
|
### Warning (Auto-Placement):
|
||||||
|
```
|
||||||
|
Parent 45 has no available leg! Finding alternative parent...
|
||||||
|
User 123 auto-placed under alternative Parent=67, Leg=Right
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error (Binary Tree Full):
|
||||||
|
```
|
||||||
|
No available parent found in network for ParentId=45
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing Scenarios
|
||||||
|
|
||||||
|
### Test 1: کاربر اول (Root)
|
||||||
|
```csharp
|
||||||
|
var command = new CreateNewUserCommand { Mobile = "09121234567" }; // No ParentId
|
||||||
|
// Result: ParentId=null, NetworkParentId=null, LegPosition=null
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Test 2: فرزند اول
|
||||||
|
```csharp
|
||||||
|
var command = new CreateNewUserCommand { Mobile = "09121234568", ParentId = 1 };
|
||||||
|
// Result: ParentId=1, NetworkParentId=1, LegPosition=Left
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Test 3: فرزند دوم
|
||||||
|
```csharp
|
||||||
|
var command = new CreateNewUserCommand { Mobile = "09121234569", ParentId = 1 };
|
||||||
|
// Result: ParentId=1, NetworkParentId=1, LegPosition=Right
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Test 4: فرزند سوم (Parent پر است)
|
||||||
|
```csharp
|
||||||
|
var command = new CreateNewUserCommand { Mobile = "09121234570", ParentId = 1 };
|
||||||
|
// Result: Auto-Placement → ParentId=1, NetworkParentId=2 (یا 3), LegPosition=Left
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Related Files
|
||||||
|
|
||||||
|
- **Service Interface**: `CMSMicroservice.Application/Common/Interfaces/INetworkPlacementService.cs`
|
||||||
|
- **Service Implementation**: `CMSMicroservice.Infrastructure/Services/NetworkPlacementService.cs`
|
||||||
|
- **Handler**: `CMSMicroservice.Application/UserCQ/Commands/CreateNewUser/CreateNewUserCommandHandler.cs`
|
||||||
|
- **DI Registration**: `CMSMicroservice.Infrastructure/ConfigureServices.cs` (خط 23)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist
|
||||||
|
|
||||||
|
- [x] `INetworkPlacementService` اضافه شد
|
||||||
|
- [x] `NetworkPlacementService` پیادهسازی شد
|
||||||
|
- [x] DI Container تنظیم شد
|
||||||
|
- [x] `CreateNewUserCommandHandler` اصلاح شد
|
||||||
|
- [ ] Unit Tests نوشته شود
|
||||||
|
- [ ] Integration Tests انجام شود
|
||||||
|
- [ ] Manual Testing با Postman/gRPC Client
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
|
||||||
|
1. **Test کردن**: ثبت چند کاربر با Parent مشابه و بررسی LegPosition
|
||||||
|
2. **Load Testing**: بررسی Performance با 10,000 کاربر
|
||||||
|
3. **Edge Cases**: تست Binary Tree Full scenario
|
||||||
|
4. **Documentation**: Update کردن API Docs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
اگر مشکلی پیش آمد:
|
||||||
|
- Log های `NetworkPlacementService` را بررسی کنید
|
||||||
|
- چک کنید که DI به درستی تنظیم شده باشد
|
||||||
|
- از `CanAcceptChildAsync` برای Pre-Validation استفاده کنید
|
||||||
1306
docs/implementation-progress-fa.md
Normal file
1306
docs/implementation-progress-fa.md
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
225
docs/migration-network-parent-guide.md
Normal file
225
docs/migration-network-parent-guide.md
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# 🔄 Migration Guide: ParentId → NetworkParentId
|
||||||
|
|
||||||
|
## 📋 Overview
|
||||||
|
|
||||||
|
در سیستم قدیمی، کاربران با استفاده از `User.ParentId` به هم متصل میشدند (Parent-Child relationship).
|
||||||
|
سیستم جدید **Network-Club-Commission** از یک **Binary Tree** استفاده میکند که نیاز به:
|
||||||
|
- `User.NetworkParentId` (شناسه پدر در شبکه باینری)
|
||||||
|
- `User.LegPosition` (Left یا Right)
|
||||||
|
|
||||||
|
برای اجرای صحیح Worker و محاسبات، **باید** تمام کاربران قدیمی Migrate شوند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Critical Issues
|
||||||
|
|
||||||
|
### مشکل 1: Binary Tree Constraint
|
||||||
|
- هر Parent فقط میتواند **2 فرزند** داشته باشد (Left & Right)
|
||||||
|
- اگر کاربری در سیستم قدیمی بیشتر از 2 فرزند دارد، Migration فقط **2 فرزند اول** را میگیرد
|
||||||
|
|
||||||
|
### مشکل 2: Orphaned Nodes
|
||||||
|
- اگر `ParentId` اشاره به یک کاربر نامعتبر (حذف شده) باشد، آن User **Orphaned** است
|
||||||
|
- Orphaned nodes در Binary Tree نادیده گرفته میشوند
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Migration Methods
|
||||||
|
|
||||||
|
### روش 1: Automatic (Seeder - توصیه میشود)
|
||||||
|
|
||||||
|
Migration به صورت خودکار در `Program.cs` در حالت **Development** اجرا میشود:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// در Program.cs
|
||||||
|
var migrationSeeder = new NetworkParentIdMigrationSeeder(dbContext, logger);
|
||||||
|
await migrationSeeder.SeedAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
**مزایا:**
|
||||||
|
- ✅ Idempotent (میتوان چندین بار اجرا کرد، فقط یکبار تاثیر میگذارد)
|
||||||
|
- ✅ Validation اتوماتیک
|
||||||
|
- ✅ Logging کامل
|
||||||
|
|
||||||
|
**کجا اجرا میشود؟**
|
||||||
|
- فقط در **Development** environment
|
||||||
|
- هر بار که پروژه Run شود
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### روش 2: Manual (Command)
|
||||||
|
|
||||||
|
اگر نیاز به اجرای دستی دارید:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// درخواست از طریق MediatR
|
||||||
|
var result = await _mediator.Send(new MigrateNetworkParentIdCommand());
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Migrated: {result.MigratedCount}");
|
||||||
|
Console.WriteLine($"Skipped: {result.SkippedCount}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error: {result.Message}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### روش 3: SQL Script
|
||||||
|
|
||||||
|
برای Production یا اجرای مستقیم روی Database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# فایل: CMSMicroservice.Infrastructure/Migrations/Scripts/20250601_MigrateParentIdToNetworkParentId.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
**نکته مهم:**
|
||||||
|
قبل از اجرا، **حتماً** بررسی کنید که آیا کاربری بیش از 2 فرزند دارد:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
ParentId,
|
||||||
|
COUNT(*) as ChildCount,
|
||||||
|
STRING_AGG(CAST(Id AS VARCHAR), ', ') as ChildIds
|
||||||
|
FROM Users
|
||||||
|
WHERE ParentId IS NOT NULL
|
||||||
|
GROUP BY ParentId
|
||||||
|
HAVING COUNT(*) > 2;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Validation After Migration
|
||||||
|
|
||||||
|
### 1. بررسی تعداد کاربران Migrate شده
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var stats = await _context.Users
|
||||||
|
.GroupBy(u => 1)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
TotalUsers = g.Count(),
|
||||||
|
UsersWithNetworkParent = g.Count(u => u.NetworkParentId != null),
|
||||||
|
LeftChildren = g.Count(u => u.LegPosition == NetworkLeg.Left),
|
||||||
|
RightChildren = g.Count(u => u.LegPosition == NetworkLeg.Right)
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. بررسی Orphaned Nodes
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT Id, NetworkParentId
|
||||||
|
FROM Users
|
||||||
|
WHERE NetworkParentId IS NOT NULL
|
||||||
|
AND NetworkParentId NOT IN (SELECT Id FROM Users);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. بررسی Binary Tree Violation
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT NetworkParentId, COUNT(*) as ChildCount
|
||||||
|
FROM Users
|
||||||
|
WHERE NetworkParentId IS NOT NULL
|
||||||
|
GROUP BY NetworkParentId
|
||||||
|
HAVING COUNT(*) > 2;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Algorithm Details
|
||||||
|
|
||||||
|
### مراحل Migration:
|
||||||
|
|
||||||
|
1. **Find Users**: یافتن کاربران با `ParentId != NULL` و `NetworkParentId == NULL`
|
||||||
|
2. **Group by Parent**: گروهبندی بر اساس ParentId
|
||||||
|
3. **Check Constraint**: اگر Parent بیش از 2 فرزند دارد، فقط 2 تا اول را بگیر
|
||||||
|
4. **Assign Values**:
|
||||||
|
```csharp
|
||||||
|
child.NetworkParentId = parentId;
|
||||||
|
child.LegPosition = (i == 0) ? NetworkLeg.Left : NetworkLeg.Right;
|
||||||
|
```
|
||||||
|
5. **Save & Validate**: ذخیره و اعتبارسنجی Binary Tree
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### مشکل: Parent has more than 2 children
|
||||||
|
|
||||||
|
**راه حل:**
|
||||||
|
تصمیم دستی بگیرید که کدام 2 فرزند را نگه دارید:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- بررسی کنید که کدام Parent مشکل دارد
|
||||||
|
SELECT ParentId, COUNT(*) as ChildCount
|
||||||
|
FROM Users
|
||||||
|
WHERE ParentId = 123
|
||||||
|
GROUP BY ParentId;
|
||||||
|
|
||||||
|
-- لیست فرزندان را ببینید
|
||||||
|
SELECT Id, FullName, CreatedAt
|
||||||
|
FROM Users
|
||||||
|
WHERE ParentId = 123
|
||||||
|
ORDER BY CreatedAt;
|
||||||
|
|
||||||
|
-- دستی NetworkParentId را برای 2 فرزند انتخابی Set کنید
|
||||||
|
UPDATE Users
|
||||||
|
SET NetworkParentId = 123, LegPosition = 0 -- Left
|
||||||
|
WHERE Id = 456;
|
||||||
|
|
||||||
|
UPDATE Users
|
||||||
|
SET NetworkParentId = 123, LegPosition = 1 -- Right
|
||||||
|
WHERE Id = 789;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### مشکل: Orphaned Nodes (Parent doesn't exist)
|
||||||
|
|
||||||
|
**راه حل:**
|
||||||
|
ParentId را NULL کنید یا به یک Parent معتبر متصل کنید:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- گزینه 1: NULL کردن (Root شدن)
|
||||||
|
UPDATE Users
|
||||||
|
SET ParentId = NULL, NetworkParentId = NULL
|
||||||
|
WHERE ParentId = 999; -- 999 وجود ندارد
|
||||||
|
|
||||||
|
-- گزینه 2: اتصال به Parent دیگر
|
||||||
|
UPDATE Users
|
||||||
|
SET ParentId = 1, NetworkParentId = 1
|
||||||
|
WHERE ParentId = 999;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist Before Production
|
||||||
|
|
||||||
|
- [ ] Migration در Development اجرا شده؟
|
||||||
|
- [ ] Validation Errors بررسی شد؟
|
||||||
|
- [ ] Orphaned Nodes رفع شدند؟
|
||||||
|
- [ ] Binary Tree Violations رفع شدند؟
|
||||||
|
- [ ] Backup از Database گرفته شده؟
|
||||||
|
- [ ] Migration Script برای Production آماده است؟
|
||||||
|
- [ ] Testing کامل انجام شده؟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Related Files
|
||||||
|
|
||||||
|
- **Seeder**: `CMSMicroservice.Infrastructure/Data/Seeding/NetworkParentIdMigrationSeeder.cs`
|
||||||
|
- **Command**: `CMSMicroservice.Application/UserCQ/Commands/MigrateNetworkParentId/`
|
||||||
|
- **SQL Script**: `CMSMicroservice.Infrastructure/Migrations/Scripts/20250601_MigrateParentIdToNetworkParentId.sql`
|
||||||
|
- **Entity**: `CMSMicroservice.Domain/Entities/User.cs` (خطوط 16, 45, 49)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
اگر مشکل خاصی با Migration پیدا کردید:
|
||||||
|
1. Log های Seeder را بررسی کنید
|
||||||
|
2. ValidationErrors را چک کنید
|
||||||
|
3. SQL Script را به صورت دستی اجرا کنید
|
||||||
732
docs/monitoring-alerts-consolidated-report.md
Normal file
732
docs/monitoring-alerts-consolidated-report.md
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
# 📊 Monitoring & Alerts System - Consolidated Implementation Report
|
||||||
|
|
||||||
|
**Date**: 2025-11-30
|
||||||
|
**Status**: ✅ Skeleton Implemented (30% Complete)
|
||||||
|
**Build**: ✅ Success
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Executive Summary
|
||||||
|
|
||||||
|
اسکلت کامل سیستم Monitoring & Alerts پیادهسازی شد. این سیستم شامل دو بخش اصلی است:
|
||||||
|
1. **Alert System**: اعلانهای مدیریتی (Critical/Warning/Success) برای Admin
|
||||||
|
2. **User Notification System**: اعلانهای کاربری (SMS/Email/Push) برای Users
|
||||||
|
|
||||||
|
فعلاً فقط Logging فعال است. Integration های اصلی (Sentry, Slack, SMS) آماده پیادهسازی هستند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Application Layer │
|
||||||
|
│ ┌─────────────────────┐ ┌─────────────────────────┐ │
|
||||||
|
│ │ IAlertService │ │ IUserNotificationService│ │
|
||||||
|
│ │ - Critical │ │ - Commission Received │ │
|
||||||
|
│ │ - Warning │ │ - Club Activation │ │
|
||||||
|
│ │ - Success │ │ - Payout Error │ │
|
||||||
|
│ └─────────────────────┘ └─────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
↓ implements
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Infrastructure Layer │
|
||||||
|
│ ┌─────────────────────┐ ┌─────────────────────────┐ │
|
||||||
|
│ │ AlertService │ │ UserNotificationService │ │
|
||||||
|
│ │ ✅ Logging │ │ ✅ Logging │ │
|
||||||
|
│ │ ⏳ Sentry │ │ ⏳ SMS Gateway │ │
|
||||||
|
│ │ ⏳ Slack │ │ ⏳ Email Service │ │
|
||||||
|
│ │ ⏳ Email │ │ ⏳ Push Notification │ │
|
||||||
|
│ └─────────────────────┘ └─────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ MonitoringSettings (Configuration) │ │
|
||||||
|
│ │ - SentryEnabled, SentryDsn │ │
|
||||||
|
│ │ - SlackEnabled, SlackWebhookUrl │ │
|
||||||
|
│ │ - EmailAlertsEnabled, AdminEmails │ │
|
||||||
|
│ │ - SmsNotificationsEnabled, SmsApiKey │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
↓ used by
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Background Workers / Handlers │
|
||||||
|
│ ┌──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ WeeklyNetworkCommissionWorker │ │
|
||||||
|
│ │ - On Success: SendSuccessNotificationAsync() │ │
|
||||||
|
│ │ - On Error: SendCriticalAlertAsync() │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
│ ┌──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ProcessUserPayoutsCommandHandler │ │
|
||||||
|
│ │ - On Payout: SendCommissionReceivedNotification│ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Implementation Details
|
||||||
|
|
||||||
|
### 1️⃣ Alert Service (Admin Notifications)
|
||||||
|
|
||||||
|
**Interface**: `CMSMicroservice.Application/Common/Interfaces/IAlertService.cs`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IAlertService
|
||||||
|
{
|
||||||
|
Task SendCriticalAlertAsync(string title, string message, Exception? exception, CancellationToken ct);
|
||||||
|
Task SendWarningAlertAsync(string title, string message, CancellationToken ct);
|
||||||
|
Task SendSuccessNotificationAsync(string title, string message, CancellationToken ct);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation**: `CMSMicroservice.Infrastructure/Services/Monitoring/AlertService.cs`
|
||||||
|
|
||||||
|
**Current Behavior**:
|
||||||
|
```
|
||||||
|
🚨 CRITICAL ALERT: {Title} - {Message}
|
||||||
|
⚠️ WARNING ALERT: {Title} - {Message}
|
||||||
|
✅ SUCCESS: {Title} - {Message}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pending Integrations**:
|
||||||
|
- **Sentry**: Exception tracking & aggregation (TODO: `SentrySdk.CaptureException()`)
|
||||||
|
- **Slack**: Real-time alerts to channel (TODO: HTTP POST to webhook)
|
||||||
|
- **Email**: Alert emails to admin list (TODO: SMTP integration)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2️⃣ User Notification Service
|
||||||
|
|
||||||
|
**Interface**: `CMSMicroservice.Application/Common/Interfaces/IAlertService.cs` (same file)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IUserNotificationService
|
||||||
|
{
|
||||||
|
Task SendCommissionReceivedNotificationAsync(long userId, decimal amount, int weekNumber, CancellationToken ct);
|
||||||
|
Task SendClubActivationNotificationAsync(long userId, CancellationToken ct);
|
||||||
|
Task SendPayoutErrorNotificationAsync(long userId, string errorMessage, CancellationToken ct);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation**: `CMSMicroservice.Infrastructure/Services/Monitoring/UserNotificationService.cs`
|
||||||
|
|
||||||
|
**Current Behavior**:
|
||||||
|
```
|
||||||
|
📧 Sending commission notification: User={UserId}, Amount={Amount}, Week={WeekNumber}
|
||||||
|
🎉 Sending club activation notification: User={UserId}
|
||||||
|
⚠️ Sending payout error notification: User={UserId}, Error={Error}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pending Integrations**:
|
||||||
|
- **SMS Gateway**: Kavenegar/Ghasedak integration (TODO: HTTP API call)
|
||||||
|
- **Email Service**: SMTP/SendGrid integration (TODO: template-based emails)
|
||||||
|
- **Push Notification**: FCM/OneSignal integration (TODO: mobile app notifications)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3️⃣ Configuration Model
|
||||||
|
|
||||||
|
**File**: `CMSMicroservice.Infrastructure/Services/Monitoring/MonitoringSettings.cs`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class MonitoringSettings
|
||||||
|
{
|
||||||
|
public const string SectionName = "Monitoring";
|
||||||
|
|
||||||
|
// Sentry
|
||||||
|
public bool SentryEnabled { get; set; }
|
||||||
|
public string? SentryDsn { get; set; }
|
||||||
|
|
||||||
|
// Slack
|
||||||
|
public bool SlackEnabled { get; set; }
|
||||||
|
public string? SlackWebhookUrl { get; set; }
|
||||||
|
|
||||||
|
// Email Alerts (Admin)
|
||||||
|
public bool EmailAlertsEnabled { get; set; }
|
||||||
|
public List<string> AdminEmails { get; set; }
|
||||||
|
|
||||||
|
// SMS (User Notifications)
|
||||||
|
public bool SmsNotificationsEnabled { get; set; }
|
||||||
|
public string? SmsApiKey { get; set; }
|
||||||
|
public string? SmsGatewayUrl { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Config File**: `CMSMicroservice.WebApi/appsettings.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Monitoring": {
|
||||||
|
"SentryEnabled": false,
|
||||||
|
"SentryDsn": "",
|
||||||
|
"SlackEnabled": false,
|
||||||
|
"SlackWebhookUrl": "",
|
||||||
|
"EmailAlertsEnabled": false,
|
||||||
|
"AdminEmails": ["admin@example.com"],
|
||||||
|
"SmsNotificationsEnabled": false,
|
||||||
|
"SmsApiKey": "",
|
||||||
|
"SmsGatewayUrl": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4️⃣ Dependency Injection
|
||||||
|
|
||||||
|
**File**: `CMSMicroservice.Infrastructure/ConfigureServices.cs`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
services.AddScoped<IAlertService, AlertService>();
|
||||||
|
services.AddScoped<IUserNotificationService, UserNotificationService>();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5️⃣ Worker Integration
|
||||||
|
|
||||||
|
**File**: `CMSMicroservice.Infrastructure/BackgroundJobs/WeeklyNetworkCommissionWorker.cs`
|
||||||
|
|
||||||
|
**On Success**:
|
||||||
|
```csharp
|
||||||
|
await alertService.SendSuccessNotificationAsync(
|
||||||
|
"Weekly Commission Completed",
|
||||||
|
$"Week {previousWeekNumber}: {payoutsProcessed} payouts, {balancesToExpire.Count} balances expired");
|
||||||
|
```
|
||||||
|
|
||||||
|
**On Error**:
|
||||||
|
```csharp
|
||||||
|
await alertService.SendCriticalAlertAsync(
|
||||||
|
"Weekly Commission Worker Failed",
|
||||||
|
$"Worker execution {executionId} failed for week {GetPreviousWeekNumber()}",
|
||||||
|
ex,
|
||||||
|
cancellationToken);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 Integration Roadmap
|
||||||
|
|
||||||
|
### Priority 1: Sentry (High - 1 hour)
|
||||||
|
|
||||||
|
**Why**: Critical error tracking & aggregation برای Production
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Install NuGet:
|
||||||
|
```bash
|
||||||
|
dotnet add package Sentry.AspNetCore
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Configure in `Program.cs`:
|
||||||
|
```csharp
|
||||||
|
builder.WebHost.UseSentry(options =>
|
||||||
|
{
|
||||||
|
options.Dsn = builder.Configuration["Monitoring:SentryDsn"];
|
||||||
|
options.Environment = builder.Environment.EnvironmentName;
|
||||||
|
options.TracesSampleRate = 1.0;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Update `AlertService.SendCriticalAlertAsync()`:
|
||||||
|
```csharp
|
||||||
|
if (_settings.SentryEnabled && exception != null)
|
||||||
|
{
|
||||||
|
SentrySdk.CaptureException(exception, scope =>
|
||||||
|
{
|
||||||
|
scope.SetTag("alert.title", title);
|
||||||
|
scope.SetExtra("message", message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Set DSN in `appsettings.Production.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Monitoring": {
|
||||||
|
"SentryEnabled": true,
|
||||||
|
"SentryDsn": "https://xxxxx@sentry.io/12345"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Priority 2: Slack Webhook (Medium - 2 hours)
|
||||||
|
|
||||||
|
**Why**: Real-time alerts به تیم Development/DevOps
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Create Incoming Webhook در Slack:
|
||||||
|
- Go to: `https://api.slack.com/apps`
|
||||||
|
- Create app → Incoming Webhooks → Add to channel
|
||||||
|
- Copy Webhook URL
|
||||||
|
|
||||||
|
2. Update `AlertService`:
|
||||||
|
```csharp
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public async Task SendCriticalAlertAsync(...)
|
||||||
|
{
|
||||||
|
_logger.LogCritical(exception, "🚨 {Title} - {Message}", title, message);
|
||||||
|
|
||||||
|
if (_settings.SlackEnabled)
|
||||||
|
{
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
text = $"🚨 *{title}*",
|
||||||
|
attachments = new[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
color = "danger",
|
||||||
|
text = message,
|
||||||
|
fields = exception != null ? new[]
|
||||||
|
{
|
||||||
|
new { title = "Exception", value = exception.Message, @short = false }
|
||||||
|
} : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _httpClient.PostAsJsonAsync(_settings.SlackWebhookUrl, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Set Webhook URL in config:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Monitoring": {
|
||||||
|
"SlackEnabled": true,
|
||||||
|
"SlackWebhookUrl": "https://hooks.slack.com/services/T00/B00/XXX"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Priority 3: SMS Gateway - Kavenegar (Medium - 3 hours)
|
||||||
|
|
||||||
|
**Why**: اطلاعرسانی کمیسیون به کاربران
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Get API Key from Kavenegar:
|
||||||
|
- Sign up: `https://panel.kavenegar.com`
|
||||||
|
- API Key: Settings → API Key
|
||||||
|
|
||||||
|
2. Create `ISmsGatewayService`:
|
||||||
|
```csharp
|
||||||
|
public interface ISmsGatewayService
|
||||||
|
{
|
||||||
|
Task SendAsync(string mobile, string message, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Implement `KavenegarSmsService`:
|
||||||
|
```csharp
|
||||||
|
public class KavenegarSmsService : ISmsGatewayService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _apiKey;
|
||||||
|
|
||||||
|
public async Task SendAsync(string mobile, string message, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var url = $"https://api.kavenegar.com/v1/{_apiKey}/sms/send.json";
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
receptor = mobile,
|
||||||
|
message = message
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(url, payload, ct);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Update `UserNotificationService.SendCommissionReceivedNotificationAsync()`:
|
||||||
|
```csharp
|
||||||
|
var user = await _context.Users.FindAsync(userId, ct);
|
||||||
|
|
||||||
|
if (user.SmsNotifications && _settings.SmsNotificationsEnabled)
|
||||||
|
{
|
||||||
|
var message = $"کمیسیون شما: {amount:N0} ریال برای هفته {weekNumber} واریز شد.";
|
||||||
|
await _smsGateway.SendAsync(user.Mobile, message, ct);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Configure:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Monitoring": {
|
||||||
|
"SmsNotificationsEnabled": true,
|
||||||
|
"SmsApiKey": "your-kavenegar-api-key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Priority 4: Email Alerts for Admins (Low - 2 hours)
|
||||||
|
|
||||||
|
**Why**: Backup notification channel
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
- **A) MailKit (SMTP)**:
|
||||||
|
```csharp
|
||||||
|
using var client = new SmtpClient();
|
||||||
|
await client.ConnectAsync("smtp.gmail.com", 587, SecureSocketOptions.StartTls);
|
||||||
|
await client.AuthenticateAsync("user@example.com", "password");
|
||||||
|
|
||||||
|
var message = new MimeMessage();
|
||||||
|
message.From.Add(new MailboxAddress("CMS Alerts", "noreply@foursat.ir"));
|
||||||
|
message.To.Add(new MailboxAddress("Admin", adminEmail));
|
||||||
|
message.Subject = $"[ALERT] {title}";
|
||||||
|
message.Body = new TextPart("html") { Text = htmlMessage };
|
||||||
|
|
||||||
|
await client.SendAsync(message);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **B) SendGrid API**:
|
||||||
|
```csharp
|
||||||
|
var client = new SendGridClient(_settings.SendGridApiKey);
|
||||||
|
var msg = MailHelper.CreateSingleEmail(
|
||||||
|
from: new EmailAddress("noreply@foursat.ir", "CMS Alerts"),
|
||||||
|
to: new EmailAddress(adminEmail),
|
||||||
|
subject: $"[ALERT] {title}",
|
||||||
|
plainTextContent: message,
|
||||||
|
htmlContent: htmlMessage
|
||||||
|
);
|
||||||
|
await client.SendEmailAsync(msg);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Config**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Monitoring": {
|
||||||
|
"EmailAlertsEnabled": true,
|
||||||
|
"AdminEmails": ["admin@foursat.ir", "devops@foursat.ir"],
|
||||||
|
"SmtpServer": "smtp.gmail.com",
|
||||||
|
"SmtpPort": 587,
|
||||||
|
"SmtpUsername": "user@example.com",
|
||||||
|
"SmtpPassword": "password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Priority 5: Retry Logic با Exponential Backoff (Low - 1 hour)
|
||||||
|
|
||||||
|
**Why**: بهبود Reliability در صورت خطاهای Transient
|
||||||
|
|
||||||
|
**Implementation در Worker**:
|
||||||
|
```csharp
|
||||||
|
private async Task<T> RetryWithExponentialBackoffAsync<T>(
|
||||||
|
Func<Task<T>> operation,
|
||||||
|
int maxRetries = 3,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
for (int attempt = 0; attempt <= maxRetries; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await operation();
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (attempt < maxRetries && IsTransientError(ex))
|
||||||
|
{
|
||||||
|
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)); // 2^n: 1s, 2s, 4s
|
||||||
|
|
||||||
|
_logger.LogWarning(ex,
|
||||||
|
"Attempt {Attempt}/{MaxRetries} failed. Retrying in {Delay}s...",
|
||||||
|
attempt + 1, maxRetries, delay.TotalSeconds);
|
||||||
|
|
||||||
|
await Task.Delay(delay, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Operation failed after {maxRetries} retries");
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsTransientError(Exception ex)
|
||||||
|
{
|
||||||
|
return ex is TimeoutException
|
||||||
|
|| ex is HttpRequestException
|
||||||
|
|| (ex is SqlException sqlEx && sqlEx.IsTransient);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage**:
|
||||||
|
```csharp
|
||||||
|
// در ExecuteWeeklyCalculationAsync():
|
||||||
|
var balancesCalculated = await RetryWithExponentialBackoffAsync(async () =>
|
||||||
|
{
|
||||||
|
return await mediator.Send(new CalculateWeeklyBalancesCommand
|
||||||
|
{
|
||||||
|
WeekNumber = previousWeekNumber
|
||||||
|
}, cancellationToken);
|
||||||
|
}, maxRetries: 3, ct: cancellationToken);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing Guide
|
||||||
|
|
||||||
|
### Test 1: Alert Service (Console Logging)
|
||||||
|
```csharp
|
||||||
|
// در Controller یا Handler:
|
||||||
|
var alertService = _serviceProvider.GetRequiredService<IAlertService>();
|
||||||
|
|
||||||
|
await alertService.SendCriticalAlertAsync(
|
||||||
|
"Test Critical Alert",
|
||||||
|
"این یک تست برای Alert Service است",
|
||||||
|
new Exception("Sample exception"));
|
||||||
|
|
||||||
|
await alertService.SendSuccessNotificationAsync(
|
||||||
|
"Test Success",
|
||||||
|
"عملیات با موفقیت انجام شد");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Output**:
|
||||||
|
```
|
||||||
|
🚨 CRITICAL ALERT: Test Critical Alert - این یک تست برای Alert Service است
|
||||||
|
✅ SUCCESS: Test Success - عملیات با موفقیت انجام شد
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Test 2: User Notification Service
|
||||||
|
```csharp
|
||||||
|
var notificationService = _serviceProvider.GetRequiredService<IUserNotificationService>();
|
||||||
|
|
||||||
|
await notificationService.SendCommissionReceivedNotificationAsync(
|
||||||
|
userId: 123,
|
||||||
|
amount: 500_000,
|
||||||
|
weekNumber: 48);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Output**:
|
||||||
|
```
|
||||||
|
📧 Sending commission notification: User=123, Amount=500000, Week=48
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Test 3: Worker Integration
|
||||||
|
```bash
|
||||||
|
# Run Worker manually (for testing)
|
||||||
|
# تغییر زمان اجرا به 1 دقیقه بعد برای تست:
|
||||||
|
# در Worker: var delay = TimeSpan.FromMinutes(1);
|
||||||
|
dotnet run --project CMSMicroservice.WebApi
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- Worker starts
|
||||||
|
- After 1 minute → Executes calculation
|
||||||
|
- On success → Logs: `✅ SUCCESS: Weekly Commission Completed`
|
||||||
|
- On error → Logs: `🚨 CRITICAL ALERT: Weekly Commission Worker Failed`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Test 4: Sentry Integration (بعد از پیادهسازی)
|
||||||
|
```csharp
|
||||||
|
// Throw یک exception برای تست:
|
||||||
|
throw new InvalidOperationException("Test Sentry integration");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check**: Sentry dashboard → Issues → باید exception جدید نمایش داده شود
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Test 5: Slack Integration (بعد از پیادهسازی)
|
||||||
|
```csharp
|
||||||
|
await alertService.SendCriticalAlertAsync("Test Slack", "Testing webhook integration", null);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check**: Slack channel → باید پیام جدید نمایش داده شود
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Test 6: SMS Integration (بعد از پیادهسازی)
|
||||||
|
```csharp
|
||||||
|
await notificationService.SendCommissionReceivedNotificationAsync(
|
||||||
|
userId: YOUR_USER_ID, // با شماره موبایل معتبر
|
||||||
|
amount: 100_000,
|
||||||
|
weekNumber: 48);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check**: موبایل کاربر → باید SMS دریافت شود
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Current Status & Progress
|
||||||
|
|
||||||
|
| Component | Status | Completion | Notes |
|
||||||
|
|-----------|--------|------------|-------|
|
||||||
|
| **Interfaces** | ✅ Done | 100% | `IAlertService`, `IUserNotificationService` |
|
||||||
|
| **Skeleton Implementations** | ✅ Done | 100% | Logging only |
|
||||||
|
| **Configuration Model** | ✅ Done | 100% | `MonitoringSettings` |
|
||||||
|
| **DI Registration** | ✅ Done | 100% | In `ConfigureServices.cs` |
|
||||||
|
| **Worker Integration** | ✅ Done | 100% | Success + Error alerts |
|
||||||
|
| **appsettings Structure** | ✅ Done | 100% | Monitoring section added |
|
||||||
|
| **Sentry Integration** | ⏳ Pending | 0% | Install package + configure DSN |
|
||||||
|
| **Slack Webhook** | ⏳ Pending | 0% | Create webhook + implement POST |
|
||||||
|
| **SMS Gateway** | ⏳ Pending | 0% | Choose provider + get API key |
|
||||||
|
| **Email Alerts** | ⏳ Pending | 0% | SMTP/SendGrid integration |
|
||||||
|
| **Retry Logic** | ⏳ Pending | 0% | Exponential backoff implementation |
|
||||||
|
| **Testing** | ⏳ Pending | 0% | Unit + Integration tests |
|
||||||
|
|
||||||
|
**Overall Progress**: 30% ✅ | 70% ⏳
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Important Notes
|
||||||
|
|
||||||
|
### 1. Production Readiness
|
||||||
|
- ⚠️ **فعلاً فقط Logging فعال است**
|
||||||
|
- ⚠️ برای Production **حداقل Sentry** باید فعال شود
|
||||||
|
- ⚠️ برای Critical systems حتماً Slack هم اضافه شود
|
||||||
|
|
||||||
|
### 2. User Preferences
|
||||||
|
- SMS/Email/Push باید بر اساس تنظیمات کاربر (`User.SmsNotifications`, etc.) ارسال شود
|
||||||
|
- در `UserNotificationService` باید ابتدا preferences چک شود
|
||||||
|
|
||||||
|
### 3. Rate Limiting
|
||||||
|
- برای SMS Gateway باید Rate Limiting در نظر گرفته شود
|
||||||
|
- پیشنهاد: استفاده از Queue (Hangfire/RabbitMQ) برای ارسال تعداد زیاد SMS
|
||||||
|
|
||||||
|
### 4. Cost Management
|
||||||
|
- SMS و Email هزینه دارند
|
||||||
|
- پیشنهاد: Batching برای ارسال گروهی
|
||||||
|
- پیشنهاد: Template-based messaging برای کاهش هزینه
|
||||||
|
|
||||||
|
### 5. Security
|
||||||
|
- API Keys در `appsettings.json` نباید commit شوند
|
||||||
|
- استفاده از Environment Variables یا Azure Key Vault
|
||||||
|
- مثال: `SmsApiKey: ${SMS_API_KEY}` در appsettings
|
||||||
|
|
||||||
|
### 6. Monitoring the Monitor
|
||||||
|
- خود Alert System هم باید Monitor شود
|
||||||
|
- اگر Slack/SMS fail شد، باید Fallback به Email یا Log باشد
|
||||||
|
- پیشنهاد: Dead Letter Queue برای failed notifications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 File Reference Map
|
||||||
|
|
||||||
|
```
|
||||||
|
CMS/
|
||||||
|
├── src/
|
||||||
|
│ ├── CMSMicroservice.Application/
|
||||||
|
│ │ └── Common/
|
||||||
|
│ │ └── Interfaces/
|
||||||
|
│ │ └── IAlertService.cs ⭐
|
||||||
|
│ │
|
||||||
|
│ ├── CMSMicroservice.Infrastructure/
|
||||||
|
│ │ ├── Services/
|
||||||
|
│ │ │ └── Monitoring/
|
||||||
|
│ │ │ ├── AlertService.cs ⭐
|
||||||
|
│ │ │ ├── UserNotificationService.cs ⭐
|
||||||
|
│ │ │ └── MonitoringSettings.cs ⭐
|
||||||
|
│ │ │
|
||||||
|
│ │ ├── BackgroundJobs/
|
||||||
|
│ │ │ └── WeeklyNetworkCommissionWorker.cs ✏️ (Modified)
|
||||||
|
│ │ │
|
||||||
|
│ │ └── ConfigureServices.cs ✏️ (Modified)
|
||||||
|
│ │
|
||||||
|
│ └── CMSMicroservice.WebApi/
|
||||||
|
│ └── appsettings.json ✏️ (Modified)
|
||||||
|
│
|
||||||
|
└── docs/
|
||||||
|
└── monitoring-alerts-implementation-report.md 📄 (This file)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Legend**:
|
||||||
|
- ⭐ = New file created
|
||||||
|
- ✏️ = Existing file modified
|
||||||
|
- 📄 = Documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Next Action Items
|
||||||
|
|
||||||
|
### Immediate (این هفته):
|
||||||
|
1. ✅ Review this document
|
||||||
|
2. ⏳ Decision: کدام Integration اول؟ (پیشنهاد: Sentry)
|
||||||
|
3. ⏳ Get credentials:
|
||||||
|
- Sentry DSN
|
||||||
|
- Slack Webhook URL
|
||||||
|
- SMS Gateway API Key
|
||||||
|
|
||||||
|
### Short-term (هفته آینده):
|
||||||
|
4. ⏳ Implement Sentry integration
|
||||||
|
5. ⏳ Implement Slack webhook
|
||||||
|
6. ⏳ Test in Staging environment
|
||||||
|
|
||||||
|
### Long-term (ماه آینده):
|
||||||
|
7. ⏳ Implement SMS Gateway (Kavenegar)
|
||||||
|
8. ⏳ Add Email alerts
|
||||||
|
9. ⏳ Implement Retry logic
|
||||||
|
10. ⏳ Write Unit/Integration tests
|
||||||
|
11. ⏳ Deploy to Production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Contact & Support
|
||||||
|
|
||||||
|
**Implementation Questions**:
|
||||||
|
- Developer: GitHub Copilot (این گزارش)
|
||||||
|
- Review: Development Team
|
||||||
|
|
||||||
|
**Service Providers**:
|
||||||
|
- **Sentry**: https://sentry.io (Error tracking)
|
||||||
|
- **Slack**: https://api.slack.com/messaging/webhooks (Webhooks)
|
||||||
|
- **Kavenegar**: https://kavenegar.com (SMS Gateway - Iran)
|
||||||
|
- **Ghasedak**: https://ghasedak.me (SMS Gateway Alternative)
|
||||||
|
- **SendGrid**: https://sendgrid.com (Email service)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2025-11-30
|
||||||
|
**Build Status**: ✅ Success
|
||||||
|
**Ready for**: Integration implementation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 TL;DR (خلاصه برای رجوع سریع)
|
||||||
|
|
||||||
|
### چی ساخته شد:
|
||||||
|
- ✅ `IAlertService` + `AlertService` (Admin alerts)
|
||||||
|
- ✅ `IUserNotificationService` + `UserNotificationService` (User notifications)
|
||||||
|
- ✅ `MonitoringSettings` (Configuration model)
|
||||||
|
- ✅ Worker integration (Success/Error alerts)
|
||||||
|
- ✅ DI registration
|
||||||
|
- ✅ appsettings structure
|
||||||
|
|
||||||
|
### فعلاً چی کار میکنه:
|
||||||
|
- Logging به Console (🚨 Critical, ⚠️ Warning, ✅ Success)
|
||||||
|
|
||||||
|
### چی باید اضافه بشه:
|
||||||
|
1. **Sentry** - Error tracking (Priority: High)
|
||||||
|
2. **Slack** - Real-time alerts (Priority: Medium)
|
||||||
|
3. **SMS Gateway** - User notifications (Priority: Medium)
|
||||||
|
4. **Email** - Backup channel (Priority: Low)
|
||||||
|
5. **Retry Logic** - Reliability (Priority: Low)
|
||||||
|
|
||||||
|
### کجا باید نگاه کنی:
|
||||||
|
- Interfaces: `CMSMicroservice.Application/Common/Interfaces/IAlertService.cs`
|
||||||
|
- Implementations: `CMSMicroservice.Infrastructure/Services/Monitoring/`
|
||||||
|
- Worker: `CMSMicroservice.Infrastructure/BackgroundJobs/WeeklyNetworkCommissionWorker.cs`
|
||||||
|
- Config: `CMSMicroservice.WebApi/appsettings.json`
|
||||||
|
|
||||||
|
### چطوری تست کنی:
|
||||||
|
```csharp
|
||||||
|
await alertService.SendCriticalAlertAsync("Test", "Message", null);
|
||||||
|
// Output: 🚨 CRITICAL ALERT: Test - Message
|
||||||
|
```
|
||||||
|
|
||||||
|
### بعدش چیکار کنم:
|
||||||
|
1. Get Sentry DSN → Update appsettings.Production.json
|
||||||
|
2. Install `Sentry.AspNetCore` → Configure in Program.cs
|
||||||
|
3. Update `AlertService.SendCriticalAlertAsync()` → Add `SentrySdk.CaptureException()`
|
||||||
|
4. Test → Deploy
|
||||||
333
docs/monitoring-alerts-implementation-report.md
Normal file
333
docs/monitoring-alerts-implementation-report.md
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
# 📊 Monitoring & Alerts System - Implementation Report
|
||||||
|
|
||||||
|
**Date**: 2025-11-30
|
||||||
|
**Status**: ✅ Skeleton Implemented
|
||||||
|
**Completion**: 30% (Structure ready, integrations pending)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Overview
|
||||||
|
|
||||||
|
اسکلت سیستم **Monitoring & Alerts** برای پروژه CMS پیادهسازی شد. این سیستم به دو بخش اصلی تقسیم میشود:
|
||||||
|
|
||||||
|
1. **Alert System**: برای ارسال اعلانهای مدیریتی (Critical Errors, Warnings, Success)
|
||||||
|
2. **User Notification System**: برای ارسال پیام به کاربران (کمیسیون، پرداخت، فعالسازی باشگاه)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Files Created/Modified
|
||||||
|
|
||||||
|
### ✨ New Files:
|
||||||
|
|
||||||
|
1. **`IAlertService.cs`** (Interface)
|
||||||
|
- `SendCriticalAlertAsync()` - برای خطاهای Critical
|
||||||
|
- `SendWarningAlertAsync()` - برای Warning ها
|
||||||
|
- `SendSuccessNotificationAsync()` - برای موفقیتها
|
||||||
|
|
||||||
|
2. **`IUserNotificationService.cs`** (Interface)
|
||||||
|
- `SendCommissionReceivedNotificationAsync()` - اعلان دریافت کمیسیون
|
||||||
|
- `SendClubActivationNotificationAsync()` - اعلان فعالسازی باشگاه
|
||||||
|
- `SendPayoutErrorNotificationAsync()` - اعلان خطا در پرداخت
|
||||||
|
|
||||||
|
3. **`AlertService.cs`** (Implementation - Skeleton)
|
||||||
|
- ✅ Logging به Console
|
||||||
|
- ⏳ TODO: Sentry Integration
|
||||||
|
- ⏳ TODO: Slack Integration
|
||||||
|
- ⏳ TODO: Email Integration
|
||||||
|
|
||||||
|
4. **`UserNotificationService.cs`** (Implementation - Skeleton)
|
||||||
|
- ✅ Logging به Console
|
||||||
|
- ⏳ TODO: SMS Gateway Integration
|
||||||
|
- ⏳ TODO: Email Service Integration
|
||||||
|
- ⏳ TODO: Push Notification Integration
|
||||||
|
|
||||||
|
5. **`MonitoringSettings.cs`** (Configuration Model)
|
||||||
|
- تنظیمات Sentry, Slack, Email, SMS
|
||||||
|
- قابل تنظیم از طریق `appsettings.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✏️ Modified Files:
|
||||||
|
|
||||||
|
1. **`ConfigureServices.cs`**
|
||||||
|
```csharp
|
||||||
|
services.AddScoped<IAlertService, AlertService>();
|
||||||
|
services.AddScoped<IUserNotificationService, UserNotificationService>();
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **`WeeklyNetworkCommissionWorker.cs`**
|
||||||
|
- ✅ Integration با `IAlertService`
|
||||||
|
- ✅ ارسال Critical Alert در صورت خطا
|
||||||
|
- ✅ ارسال Success Notification پس از اتمام موفق
|
||||||
|
|
||||||
|
3. **`appsettings.json`**
|
||||||
|
- اضافه شدن بخش `Monitoring` با تنظیمات پیشفرض
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Current Implementation
|
||||||
|
|
||||||
|
### Alert System Usage:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// در Worker یا هر Handler دیگر:
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// عملیات خطرناک
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await _alertService.SendCriticalAlertAsync(
|
||||||
|
"Operation Failed",
|
||||||
|
"Description of what went wrong",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current Output:
|
||||||
|
```
|
||||||
|
🚨 CRITICAL ALERT: Weekly Commission Worker Failed - Worker execution abc-123 failed for week 2025-W48
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⏳ Pending Integrations (TODO)
|
||||||
|
|
||||||
|
### 1. Sentry Integration
|
||||||
|
```csharp
|
||||||
|
// در AlertService.SendCriticalAlertAsync():
|
||||||
|
if (_settings.SentryEnabled)
|
||||||
|
{
|
||||||
|
SentrySdk.CaptureException(exception);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
- Install NuGet: `Sentry.AspNetCore`
|
||||||
|
- Configure DSN in `appsettings.json`
|
||||||
|
- Add to `Program.cs`: `builder.WebHost.UseSentry()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Slack Integration
|
||||||
|
```csharp
|
||||||
|
// در AlertService:
|
||||||
|
if (_settings.SlackEnabled)
|
||||||
|
{
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
text = $"🚨 {title}",
|
||||||
|
attachments = new[]
|
||||||
|
{
|
||||||
|
new { text = message, color = "danger" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _httpClient.PostAsJsonAsync(_settings.SlackWebhookUrl, payload);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
- Create Slack Incoming Webhook
|
||||||
|
- Add URL to `appsettings.json`
|
||||||
|
- Install NuGet: `System.Net.Http.Json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Email Alerts (برای Admin)
|
||||||
|
```csharp
|
||||||
|
// در AlertService:
|
||||||
|
if (_settings.EmailAlertsEnabled)
|
||||||
|
{
|
||||||
|
foreach (var email in _settings.AdminEmails)
|
||||||
|
{
|
||||||
|
await _emailService.SendAsync(
|
||||||
|
to: email,
|
||||||
|
subject: $"[ALERT] {title}",
|
||||||
|
body: message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
- Configure SMTP settings
|
||||||
|
- Install NuGet: `MailKit` or use existing email service
|
||||||
|
- Add admin emails to config
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. SMS Notifications (برای کاربران)
|
||||||
|
```csharp
|
||||||
|
// در UserNotificationService.SendCommissionReceivedNotificationAsync():
|
||||||
|
var user = await _context.Users.FindAsync(userId);
|
||||||
|
|
||||||
|
if (user.SmsNotifications && _settings.SmsNotificationsEnabled)
|
||||||
|
{
|
||||||
|
var message = $"کمیسیون شما: {amount:N0} ریال برای هفته {weekNumber} واریز شد.";
|
||||||
|
|
||||||
|
await _smsGateway.SendAsync(user.Mobile, message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
- Choose SMS provider (Kavenegar, Ghasedak, etc.)
|
||||||
|
- Get API Key
|
||||||
|
- Implement `ISmsGatewayService`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Retry Logic با Exponential Backoff
|
||||||
|
```csharp
|
||||||
|
// در Worker:
|
||||||
|
private async Task<T> RetryWithExponentialBackoff<T>(
|
||||||
|
Func<Task<T>> operation,
|
||||||
|
int maxRetries = 3)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < maxRetries; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await operation();
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (i < maxRetries - 1)
|
||||||
|
{
|
||||||
|
var delay = TimeSpan.FromSeconds(Math.Pow(2, i)); // 2^i seconds
|
||||||
|
_logger.LogWarning("Retry {Attempt}/{Max} after {Delay}s",
|
||||||
|
i + 1, maxRetries, delay.TotalSeconds);
|
||||||
|
await Task.Delay(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Configuration Example
|
||||||
|
|
||||||
|
در `appsettings.Production.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Monitoring": {
|
||||||
|
"SentryEnabled": true,
|
||||||
|
"SentryDsn": "https://xxxxx@sentry.io/12345",
|
||||||
|
|
||||||
|
"SlackEnabled": true,
|
||||||
|
"SlackWebhookUrl": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX",
|
||||||
|
|
||||||
|
"EmailAlertsEnabled": true,
|
||||||
|
"AdminEmails": [
|
||||||
|
"admin@foursat.ir",
|
||||||
|
"devops@foursat.ir"
|
||||||
|
],
|
||||||
|
|
||||||
|
"SmsNotificationsEnabled": true,
|
||||||
|
"SmsApiKey": "your-kavenegar-api-key",
|
||||||
|
"SmsGatewayUrl": "https://api.kavenegar.com/v1/{apikey}/sms/send.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Test 1: Alert Service
|
||||||
|
```csharp
|
||||||
|
var alertService = serviceProvider.GetRequiredService<IAlertService>();
|
||||||
|
|
||||||
|
await alertService.SendCriticalAlertAsync(
|
||||||
|
"Test Alert",
|
||||||
|
"This is a test critical alert");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected**: Log در Console + (در Production) Sentry + Slack
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Test 2: User Notification
|
||||||
|
```csharp
|
||||||
|
var notificationService = serviceProvider.GetRequiredService<IUserNotificationService>();
|
||||||
|
|
||||||
|
await notificationService.SendCommissionReceivedNotificationAsync(
|
||||||
|
userId: 123,
|
||||||
|
amount: 500_000,
|
||||||
|
weekNumber: 48);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected**: Log در Console + (در Production) SMS + Email
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Integration Priority
|
||||||
|
|
||||||
|
| Priority | Integration | Effort | Impact |
|
||||||
|
|----------|------------|--------|--------|
|
||||||
|
| 🔴 High | Sentry | 1 hour | Critical error tracking |
|
||||||
|
| 🟡 Medium | Slack | 2 hours | Real-time admin alerts |
|
||||||
|
| 🟡 Medium | SMS (Kavenegar) | 3 hours | User notifications |
|
||||||
|
| 🟢 Low | Email Alerts | 2 hours | Backup notification channel |
|
||||||
|
| 🟢 Low | Retry Logic | 1 hour | Reliability improvement |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Current Status Summary
|
||||||
|
|
||||||
|
### Completed (30%):
|
||||||
|
- ✅ Interface definitions
|
||||||
|
- ✅ Skeleton implementations with Logging
|
||||||
|
- ✅ DI registration
|
||||||
|
- ✅ Worker integration
|
||||||
|
- ✅ Configuration model
|
||||||
|
- ✅ appsettings structure
|
||||||
|
|
||||||
|
### Pending (70%):
|
||||||
|
- ⏳ Sentry integration (5%)
|
||||||
|
- ⏳ Slack webhook (10%)
|
||||||
|
- ⏳ Email service (10%)
|
||||||
|
- ⏳ SMS gateway (15%)
|
||||||
|
- ⏳ Push notifications (10%)
|
||||||
|
- ⏳ Retry logic (5%)
|
||||||
|
- ⏳ Testing (10%)
|
||||||
|
- ⏳ Documentation (5%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
|
||||||
|
1. **Immediate** (در صورت نیاز):
|
||||||
|
- Enable Sentry for error tracking
|
||||||
|
- Setup Slack webhook for critical alerts
|
||||||
|
|
||||||
|
2. **Short-term** (هفته آینده):
|
||||||
|
- Integrate SMS gateway (Kavenegar)
|
||||||
|
- Test User notifications
|
||||||
|
|
||||||
|
3. **Long-term** (ماه آینده):
|
||||||
|
- Add Email service
|
||||||
|
- Implement Retry logic
|
||||||
|
- Push notification service
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
- تمام TODO ها در کد با comment مشخص شدهاند
|
||||||
|
- فعلاً فقط Logging فعال است
|
||||||
|
- برای Production باید حتماً یکی از Integration ها (Sentry/Slack) فعال شود
|
||||||
|
- SMS Gateway باید بر اساس پروژه انتخاب شود (Kavenegar, Ghasedak, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Related Files
|
||||||
|
|
||||||
|
- **Interfaces**: `CMSMicroservice.Application/Common/Interfaces/IAlertService.cs`
|
||||||
|
- **Implementations**: `CMSMicroservice.Infrastructure/Services/Monitoring/`
|
||||||
|
- **Worker**: `CMSMicroservice.Infrastructure/BackgroundJobs/WeeklyNetworkCommissionWorker.cs`
|
||||||
|
- **Config**: `CMSMicroservice.WebApi/appsettings.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Report generated**: 2025-11-30
|
||||||
|
**Build Status**: ✅ Success
|
||||||
|
**Ready for**: Development continuation / Integration implementation
|
||||||
@@ -20,23 +20,37 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
throw new NotFoundException(nameof(User), request.UserId);
|
throw new NotFoundException(nameof(User), request.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// دریافت مبلغ عضویت از Configuration
|
||||||
|
var membershipPrice = await _context.SystemConfigurations
|
||||||
|
.Where(x => x.Key == "club_membership_price" && x.IsActive)
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
long initialContribution = 25_000_000; // Default: 25 million Rials
|
||||||
|
if (!string.IsNullOrEmpty(membershipPrice) && long.TryParse(membershipPrice, out var parsedPrice))
|
||||||
|
{
|
||||||
|
initialContribution = parsedPrice;
|
||||||
|
}
|
||||||
|
|
||||||
// بررسی عضویت فعلی
|
// بررسی عضویت فعلی
|
||||||
var existingMembership = await _context.ClubMemberships
|
var existingMembership = await _context.ClubMemberships
|
||||||
.FirstOrDefaultAsync(x => x.UserId == request.UserId, cancellationToken);
|
.FirstOrDefaultAsync(x => x.UserId == request.UserId, cancellationToken);
|
||||||
|
|
||||||
ClubMembership entity;
|
ClubMembership entity;
|
||||||
bool isNewMembership = existingMembership == null;
|
bool isNewMembership = existingMembership == null;
|
||||||
var activationDate = request.ActivationDate ?? DateTimeOffset.UtcNow;
|
var activationDate = request.ActivationDate ?? DateTimeOffset.Now; // استفاده از Local Time
|
||||||
|
|
||||||
if (isNewMembership)
|
if (isNewMembership)
|
||||||
{
|
{
|
||||||
// ایجاد عضویت جدید
|
// ایجاد عضویت جدید
|
||||||
|
// توجه: InitialContribution فقط ثبت میشود، از کیف پول کسر نمیشود!
|
||||||
|
// کاربر قبلاً باید کیف پول خود را شارژ کرده باشد
|
||||||
entity = new ClubMembership
|
entity = new ClubMembership
|
||||||
{
|
{
|
||||||
UserId = request.UserId,
|
UserId = request.UserId,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
ActivatedAt = activationDate.DateTime,
|
ActivatedAt = activationDate.DateTime,
|
||||||
InitialContribution = 0,
|
InitialContribution = initialContribution, // مبلغ عضویت از Configuration
|
||||||
TotalEarned = 0
|
TotalEarned = 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس ارسال Alert و Notification
|
||||||
|
/// برای ارسال اعلانهای مختلف از طریق کانالهای مختلف (Email, SMS, Slack, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public interface IAlertService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ارسال Alert برای خطاهای Critical
|
||||||
|
/// </summary>
|
||||||
|
Task SendCriticalAlertAsync(string title, string message, Exception? exception = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ارسال Alert برای Warning
|
||||||
|
/// </summary>
|
||||||
|
Task SendWarningAlertAsync(string title, string message, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ارسال اعلان موفقیت
|
||||||
|
/// </summary>
|
||||||
|
Task SendSuccessNotificationAsync(string title, string message, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس ارسال Notification به کاربران
|
||||||
|
/// برای ارسال پیامک، ایمیل و پوش به کاربران سیستم
|
||||||
|
/// </summary>
|
||||||
|
public interface IUserNotificationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ارسال اعلان دریافت کمیسیون به کاربر
|
||||||
|
/// </summary>
|
||||||
|
Task SendCommissionReceivedNotificationAsync(
|
||||||
|
long userId,
|
||||||
|
decimal amount,
|
||||||
|
int weekNumber,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ارسال اعلان فعالسازی عضویت باشگاه
|
||||||
|
/// </summary>
|
||||||
|
Task SendClubActivationNotificationAsync(
|
||||||
|
long userId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ارسال اعلان خطا در پرداخت
|
||||||
|
/// </summary>
|
||||||
|
Task SendPayoutErrorNotificationAsync(
|
||||||
|
long userId,
|
||||||
|
string errorMessage,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس محاسبه موقعیت در Binary Tree
|
||||||
|
/// این سرویس مشخص میکند که کاربر جدید باید در کدام Leg (Left/Right) قرار بگیرد
|
||||||
|
/// </summary>
|
||||||
|
public interface INetworkPlacementService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// محاسبه LegPosition برای کاربر جدید
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parentId">شناسه Parent در Network</param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns>
|
||||||
|
/// - Left: اگر Parent فرزند چپ ندارد
|
||||||
|
/// - Right: اگر Parent فرزند راست ندارد
|
||||||
|
/// - null: اگر Parent هر دو Leg را دارد (Binary Tree پر است!)
|
||||||
|
/// </returns>
|
||||||
|
Task<NetworkLeg?> CalculateLegPositionAsync(long parentId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// بررسی اینکه آیا Parent میتواند فرزند جدید بپذیرد
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parentId"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns>true اگر Parent کمتر از 2 فرزند دارد</returns>
|
||||||
|
Task<bool> CanAcceptChildAsync(long parentId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پیدا کردن اولین Parent در شبکه که میتواند فرزند جدید بپذیرد
|
||||||
|
/// (برای Auto-Placement در Binary Tree)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rootParentId">شناسه Parent اصلی که از آن شروع میکنیم</param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns>شناسه Parent مناسب برای قرار گرفتن کاربر جدید</returns>
|
||||||
|
Task<long?> FindAvailableParentAsync(long rootParentId, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -1,12 +1,23 @@
|
|||||||
using CMSMicroservice.Domain.Events;
|
using CMSMicroservice.Domain.Events;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.UserCQ.Commands.CreateNewUser;
|
namespace CMSMicroservice.Application.UserCQ.Commands.CreateNewUser;
|
||||||
|
|
||||||
public class CreateNewUserCommandHandler : IRequestHandler<CreateNewUserCommand, CreateNewUserResponseDto>
|
public class CreateNewUserCommandHandler : IRequestHandler<CreateNewUserCommand, CreateNewUserResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly INetworkPlacementService _networkPlacementService;
|
||||||
|
private readonly ILogger<CreateNewUserCommandHandler> _logger;
|
||||||
|
|
||||||
public CreateNewUserCommandHandler(IApplicationDbContext context)
|
public CreateNewUserCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
INetworkPlacementService networkPlacementService,
|
||||||
|
ILogger<CreateNewUserCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_networkPlacementService = networkPlacementService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CreateNewUserResponseDto> Handle(CreateNewUserCommand request,
|
public async Task<CreateNewUserResponseDto> Handle(CreateNewUserCommand request,
|
||||||
@@ -15,9 +26,71 @@ public class CreateNewUserCommandHandler : IRequestHandler<CreateNewUserCommand,
|
|||||||
var entity = request.Adapt<User>();
|
var entity = request.Adapt<User>();
|
||||||
entity.ReferralCode = UtilExtensions.Generate(digits: 10, firstDigitNonZero: true);
|
entity.ReferralCode = UtilExtensions.Generate(digits: 10, firstDigitNonZero: true);
|
||||||
|
|
||||||
|
// === تنظیم Network Binary Tree ===
|
||||||
|
// اگر ParentId تنظیم شده، باید NetworkParentId و LegPosition هم Set بشن
|
||||||
|
if (request.ParentId.HasValue)
|
||||||
|
{
|
||||||
|
// محاسبه LegPosition برای Binary Tree
|
||||||
|
var legPosition = await _networkPlacementService.CalculateLegPositionAsync(
|
||||||
|
request.ParentId.Value,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (legPosition.HasValue)
|
||||||
|
{
|
||||||
|
// Parent میتواند فرزند جدید بپذیرد
|
||||||
|
entity.NetworkParentId = request.ParentId.Value;
|
||||||
|
entity.LegPosition = legPosition.Value;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"User {UserId} placed in Binary Tree: Parent={ParentId}, Leg={Leg}",
|
||||||
|
entity.Id, entity.NetworkParentId, entity.LegPosition);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Parent پر است! باید Auto-Placement کنیم یا Error بدیم
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Parent {ParentId} has no available leg! Finding alternative parent...",
|
||||||
|
request.ParentId.Value);
|
||||||
|
|
||||||
|
var availableParent = await _networkPlacementService.FindAvailableParentAsync(
|
||||||
|
request.ParentId.Value,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (availableParent.HasValue)
|
||||||
|
{
|
||||||
|
var newLegPosition = await _networkPlacementService.CalculateLegPositionAsync(
|
||||||
|
availableParent.Value,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
entity.NetworkParentId = availableParent.Value;
|
||||||
|
entity.LegPosition = newLegPosition!.Value;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"User {UserId} auto-placed under alternative Parent={ParentId}, Leg={Leg}",
|
||||||
|
entity.Id, entity.NetworkParentId, entity.LegPosition);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// هیچ جای خالی در Binary Tree پیدا نشد!
|
||||||
|
_logger.LogError(
|
||||||
|
"No available parent found in network for ParentId={ParentId}",
|
||||||
|
request.ParentId.Value);
|
||||||
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"شبکه Parent با شناسه {request.ParentId.Value} پر است و نمیتواند کاربر جدید بپذیرد.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// کاربر Root است (بدون Parent)
|
||||||
|
_logger.LogInformation("Creating root user without Parent");
|
||||||
|
}
|
||||||
|
|
||||||
await _context.Users.AddAsync(entity, cancellationToken);
|
await _context.Users.AddAsync(entity, cancellationToken);
|
||||||
entity.AddDomainEvent(new CreateNewUserEvent(entity));
|
entity.AddDomainEvent(new CreateNewUserEvent(entity));
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
return entity.Adapt<CreateNewUserResponseDto>();
|
return entity.Adapt<CreateNewUserResponseDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.MigrateNetworkParentId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for manual migration of ParentId → NetworkParentId
|
||||||
|
/// این Command در صورتی که Seeder اجرا نشده یا نیاز به اجرای دستی باشد، استفاده میشود
|
||||||
|
/// </summary>
|
||||||
|
public record MigrateNetworkParentIdCommand : IRequest<MigrateNetworkParentIdResult>;
|
||||||
|
|
||||||
|
public record MigrateNetworkParentIdResult
|
||||||
|
{
|
||||||
|
public bool Success { get; init; }
|
||||||
|
public int MigratedCount { get; init; }
|
||||||
|
public int SkippedCount { get; init; }
|
||||||
|
public List<string> ValidationErrors { get; init; } = new();
|
||||||
|
public string Message { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.MigrateNetworkParentId;
|
||||||
|
|
||||||
|
public class MigrateNetworkParentIdCommandHandler : IRequestHandler<MigrateNetworkParentIdCommand, MigrateNetworkParentIdResult>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ILogger<MigrateNetworkParentIdCommandHandler> _logger;
|
||||||
|
|
||||||
|
public MigrateNetworkParentIdCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
ILogger<MigrateNetworkParentIdCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MigrateNetworkParentIdResult> Handle(MigrateNetworkParentIdCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("=== Starting Manual ParentId → NetworkParentId Migration ===");
|
||||||
|
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
// Step 1: Check if already migrated
|
||||||
|
var alreadyMigrated = await _context.Users
|
||||||
|
.Where(u => u.ParentId != null && u.NetworkParentId != null)
|
||||||
|
.AnyAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (alreadyMigrated)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("⚠️ Migration already completed!");
|
||||||
|
return new MigrateNetworkParentIdResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Migration already completed. All users with ParentId have NetworkParentId."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Find users to migrate
|
||||||
|
var usersToMigrate = await _context.Users
|
||||||
|
.Where(u => u.ParentId != null && u.NetworkParentId == null)
|
||||||
|
.OrderBy(u => u.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (usersToMigrate.Count == 0)
|
||||||
|
{
|
||||||
|
return new MigrateNetworkParentIdResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "No users to migrate. All done!"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Group by ParentId
|
||||||
|
var parentGroups = usersToMigrate.GroupBy(u => u.ParentId);
|
||||||
|
|
||||||
|
int migratedCount = 0;
|
||||||
|
int skippedCount = 0;
|
||||||
|
|
||||||
|
foreach (var group in parentGroups)
|
||||||
|
{
|
||||||
|
var parentId = group.Key;
|
||||||
|
var children = group.OrderBy(u => u.Id).ToList();
|
||||||
|
|
||||||
|
if (children.Count > 2)
|
||||||
|
{
|
||||||
|
var warning = $"Parent {parentId} has {children.Count} children! Taking first 2 only.";
|
||||||
|
_logger.LogWarning(warning);
|
||||||
|
errors.Add(warning);
|
||||||
|
|
||||||
|
skippedCount += (children.Count - 2);
|
||||||
|
children = children.Take(2).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign NetworkParentId and LegPosition
|
||||||
|
for (int i = 0; i < children.Count && i < 2; i++)
|
||||||
|
{
|
||||||
|
var child = children[i];
|
||||||
|
child.NetworkParentId = parentId;
|
||||||
|
child.LegPosition = i == 0 ? NetworkLeg.Left : NetworkLeg.Right;
|
||||||
|
migratedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Save changes
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("✅ Migration Completed! Migrated={Migrated}, Skipped={Skipped}",
|
||||||
|
migratedCount, skippedCount);
|
||||||
|
|
||||||
|
// Step 5: Validate
|
||||||
|
await ValidateAsync(errors, cancellationToken);
|
||||||
|
|
||||||
|
return new MigrateNetworkParentIdResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
MigratedCount = migratedCount,
|
||||||
|
SkippedCount = skippedCount,
|
||||||
|
ValidationErrors = errors,
|
||||||
|
Message = $"Migration completed successfully. Migrated: {migratedCount}, Skipped: {skippedCount}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidateAsync(List<string> errors, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Check orphaned nodes
|
||||||
|
var orphanedUsers = await _context.Users
|
||||||
|
.Where(u => u.NetworkParentId != null &&
|
||||||
|
!_context.Users.Any(p => p.Id == u.NetworkParentId))
|
||||||
|
.Select(u => u.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (orphanedUsers.Any())
|
||||||
|
{
|
||||||
|
var error = $"Found {orphanedUsers.Count} orphaned users: {string.Join(", ", orphanedUsers)}";
|
||||||
|
_logger.LogError(error);
|
||||||
|
errors.Add(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check binary tree violation
|
||||||
|
var parentsWithTooManyChildren = await _context.Users
|
||||||
|
.Where(u => u.NetworkParentId != null)
|
||||||
|
.GroupBy(u => u.NetworkParentId)
|
||||||
|
.Select(g => new { ParentId = g.Key, Count = g.Count() })
|
||||||
|
.Where(x => x.Count > 2)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (parentsWithTooManyChildren.Any())
|
||||||
|
{
|
||||||
|
var error = $"Binary tree violation! {parentsWithTooManyChildren.Count} parents have >2 children";
|
||||||
|
_logger.LogError(error);
|
||||||
|
errors.Add(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Transactions;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
|
||||||
|
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool;
|
||||||
|
using CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.BackgroundJobs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Background Worker برای محاسبه و توزیع کمیسیونهای هفتگی شبکه
|
||||||
|
/// زمان اجرا: هر یکشنبه ساعت 23:59
|
||||||
|
/// </summary>
|
||||||
|
public class WeeklyNetworkCommissionWorker : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly ILogger<WeeklyNetworkCommissionWorker> _logger;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly IAlertService _alertService;
|
||||||
|
private Timer? _timer;
|
||||||
|
|
||||||
|
public WeeklyNetworkCommissionWorker(
|
||||||
|
ILogger<WeeklyNetworkCommissionWorker> logger,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
IAlertService alertService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_alertService = alertService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Weekly Network Commission Worker started at: {Time} (Local Time)", DateTime.Now);
|
||||||
|
|
||||||
|
// محاسبه زمان تا یکشنبه بعدی ساعت 23:59
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var nextSunday = GetNextSunday(now);
|
||||||
|
var nextRunTime = new DateTime(nextSunday.Year, nextSunday.Month, nextSunday.Day, 23, 59, 0);
|
||||||
|
|
||||||
|
var delay = nextRunTime - now;
|
||||||
|
if (delay.TotalMilliseconds < 0)
|
||||||
|
{
|
||||||
|
// اگر زمان گذشته باشد، یکشنبه بعدی
|
||||||
|
nextRunTime = nextRunTime.AddDays(7);
|
||||||
|
delay = nextRunTime - now;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Next execution scheduled for: {NextRun}", nextRunTime);
|
||||||
|
|
||||||
|
// تنظیم timer برای اجرا در زمان مشخص و تکرار هفتگی
|
||||||
|
_timer = new Timer(
|
||||||
|
callback: async _ => await ExecuteWeeklyCalculationAsync(stoppingToken),
|
||||||
|
state: null,
|
||||||
|
dueTime: delay,
|
||||||
|
period: TimeSpan.FromDays(7) // هر 7 روز یکبار
|
||||||
|
);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// محاسبه تاریخ یکشنبه بعدی
|
||||||
|
/// </summary>
|
||||||
|
private static DateTime GetNextSunday(DateTime from)
|
||||||
|
{
|
||||||
|
var daysUntilSunday = ((int)DayOfWeek.Sunday - (int)from.DayOfWeek + 7) % 7;
|
||||||
|
if (daysUntilSunday == 0)
|
||||||
|
{
|
||||||
|
// اگر امروز یکشنبه است و ساعت گذشته، یکشنبه بعدی
|
||||||
|
if (from.TimeOfDay > new TimeSpan(23, 59, 0))
|
||||||
|
{
|
||||||
|
daysUntilSunday = 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return from.Date.AddDays(daysUntilSunday);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// اجرای محاسبات هفتگی کمیسیون
|
||||||
|
/// </summary>
|
||||||
|
private async Task ExecuteWeeklyCalculationAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var executionId = Guid.NewGuid();
|
||||||
|
var startTime = DateTime.Now; // استفاده از Local Time
|
||||||
|
_logger.LogInformation("=== Starting Weekly Commission Calculation [{ExecutionId}] at {Time} (Local Time) ===",
|
||||||
|
executionId, startTime);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<IApplicationDbContext>();
|
||||||
|
|
||||||
|
// دریافت شماره هفته قبل (هفتهای که باید محاسبه شود)
|
||||||
|
var previousWeekNumber = GetPreviousWeekNumber();
|
||||||
|
var currentWeekNumber = GetCurrentWeekNumber();
|
||||||
|
|
||||||
|
_logger.LogInformation("Processing week: {WeekNumber}", previousWeekNumber);
|
||||||
|
|
||||||
|
// ===== IDEMPOTENCY CHECK =====
|
||||||
|
// بررسی اینکه آیا این هفته قبلاً محاسبه شده یا نه
|
||||||
|
var existingPool = await context.WeeklyCommissionPools
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.WeekNumber == previousWeekNumber, cancellationToken);
|
||||||
|
|
||||||
|
if (existingPool?.IsCalculated == true)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Week {WeekNumber} already calculated. Skipping execution [{ExecutionId}]",
|
||||||
|
previousWeekNumber, executionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== TRANSACTION SCOPE =====
|
||||||
|
// تمام مراحل باید داخل یک تراکنش باشند برای Atomicity
|
||||||
|
using var transaction = new TransactionScope(
|
||||||
|
TransactionScopeOption.Required,
|
||||||
|
new TransactionOptions
|
||||||
|
{
|
||||||
|
IsolationLevel = IsolationLevel.ReadCommitted,
|
||||||
|
Timeout = TimeSpan.FromMinutes(30) // برای شبکههای بزرگ
|
||||||
|
},
|
||||||
|
TransactionScopeAsyncFlowOption.Enabled);
|
||||||
|
|
||||||
|
int balancesCalculated = 0;
|
||||||
|
long poolValue = 0;
|
||||||
|
int payoutsProcessed = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// مرحله 1: محاسبه تعادلهای شبکه
|
||||||
|
_logger.LogInformation("Step 1/4: Calculating network balances for week {WeekNumber}", previousWeekNumber);
|
||||||
|
balancesCalculated = await mediator.Send(new CalculateWeeklyBalancesCommand
|
||||||
|
{
|
||||||
|
WeekNumber = previousWeekNumber,
|
||||||
|
ForceRecalculate = false
|
||||||
|
}, cancellationToken);
|
||||||
|
_logger.LogInformation("Network balances calculated: {Count} users processed", balancesCalculated);
|
||||||
|
|
||||||
|
// مرحله 2: محاسبه استخر کمیسیون و ارزش هر امتیاز
|
||||||
|
_logger.LogInformation("Step 2/4: Calculating commission pool for week {WeekNumber}", previousWeekNumber);
|
||||||
|
poolValue = await mediator.Send(new CalculateWeeklyCommissionPoolCommand
|
||||||
|
{
|
||||||
|
WeekNumber = previousWeekNumber,
|
||||||
|
ForceRecalculate = false
|
||||||
|
}, cancellationToken);
|
||||||
|
_logger.LogInformation("Commission pool calculated. Value per balance: {Value:N0} Rials", poolValue);
|
||||||
|
|
||||||
|
// مرحله 3: توزیع کمیسیونها به کاربران
|
||||||
|
_logger.LogInformation("Step 3/4: Processing user payouts for week {WeekNumber}", previousWeekNumber);
|
||||||
|
payoutsProcessed = await mediator.Send(new ProcessUserPayoutsCommand
|
||||||
|
{
|
||||||
|
WeekNumber = previousWeekNumber,
|
||||||
|
ForceReprocess = false
|
||||||
|
}, cancellationToken);
|
||||||
|
_logger.LogInformation("User payouts processed: {Count} payouts created", payoutsProcessed);
|
||||||
|
|
||||||
|
// ===== مرحله 4 (گام 5 در مستندات): ریست/Expire کردن تعادلهای هفته قبل =====
|
||||||
|
_logger.LogInformation("Step 4/4: Expiring weekly balances for week {WeekNumber}", previousWeekNumber);
|
||||||
|
var balancesToExpire = await context.NetworkWeeklyBalances
|
||||||
|
.Where(x => x.WeekNumber == previousWeekNumber && !x.IsExpired)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
foreach (var balance in balancesToExpire)
|
||||||
|
{
|
||||||
|
balance.IsExpired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
_logger.LogInformation("Expired {Count} balance records", balancesToExpire.Count);
|
||||||
|
|
||||||
|
// Commit Transaction
|
||||||
|
transaction.Complete();
|
||||||
|
|
||||||
|
var duration = DateTime.Now - startTime; // محاسبه مدت زمان با Local Time
|
||||||
|
_logger.LogInformation(
|
||||||
|
"=== Weekly Commission Calculation Completed Successfully [{ExecutionId}] ===" +
|
||||||
|
"\n Week: {WeekNumber}" +
|
||||||
|
"\n Users Processed: {UserCount}" +
|
||||||
|
"\n Value Per Balance: {ValuePerBalance:N0} Rials" +
|
||||||
|
"\n Payouts Created: {PayoutCount}" +
|
||||||
|
"\n Balances Expired: {ExpiredCount}" +
|
||||||
|
"\n Duration: {Duration:mm\\:ss}",
|
||||||
|
executionId,
|
||||||
|
previousWeekNumber,
|
||||||
|
balancesCalculated,
|
||||||
|
poolValue,
|
||||||
|
payoutsProcessed,
|
||||||
|
balancesToExpire.Count,
|
||||||
|
duration
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send success notification to admin
|
||||||
|
using var successScope = _serviceProvider.CreateScope();
|
||||||
|
var alertService = successScope.ServiceProvider.GetRequiredService<IAlertService>();
|
||||||
|
|
||||||
|
await alertService.SendSuccessNotificationAsync(
|
||||||
|
"Weekly Commission Completed",
|
||||||
|
$"Week {previousWeekNumber}: {payoutsProcessed} payouts, {balancesToExpire.Count} balances expired");
|
||||||
|
|
||||||
|
// TODO: Send notifications to users who received commission
|
||||||
|
// await NotifyUsersAboutPayouts(payoutsProcessed, previousWeekNumber);
|
||||||
|
}
|
||||||
|
catch (Exception innerEx)
|
||||||
|
{
|
||||||
|
_logger.LogError(innerEx,
|
||||||
|
"Transaction failed during step execution. Rolling back. [{ExecutionId}]",
|
||||||
|
executionId);
|
||||||
|
// Transaction will auto-rollback when scope is disposed without Complete()
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogCritical(ex,
|
||||||
|
"!!! CRITICAL ERROR in Weekly Commission Calculation [{ExecutionId}] !!!" +
|
||||||
|
"\n Week: {WeekNumber}" +
|
||||||
|
"\n Message: {Message}" +
|
||||||
|
"\n StackTrace: {StackTrace}" +
|
||||||
|
"\n Please investigate immediately!",
|
||||||
|
executionId,
|
||||||
|
GetPreviousWeekNumber(),
|
||||||
|
ex.Message,
|
||||||
|
ex.StackTrace);
|
||||||
|
|
||||||
|
// ===== ERROR HANDLING & ALERTING =====
|
||||||
|
// در محیط production باید Alert/Notification ارسال شود
|
||||||
|
|
||||||
|
using var errorScope = _serviceProvider.CreateScope();
|
||||||
|
var alertService = errorScope.ServiceProvider.GetRequiredService<IAlertService>();
|
||||||
|
|
||||||
|
await alertService.SendCriticalAlertAsync(
|
||||||
|
"Weekly Commission Worker Failed",
|
||||||
|
$"Worker execution {executionId} failed for week {GetPreviousWeekNumber()}",
|
||||||
|
ex,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
// TODO: Retry logic با exponential backoff
|
||||||
|
// await RetryWithExponentialBackoff(() => ExecuteWeeklyCalculationAsync(cancellationToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت شماره هفته جاری (فرمت ISO 8601: YYYY-Www)
|
||||||
|
/// </summary>
|
||||||
|
private static string GetCurrentWeekNumber()
|
||||||
|
{
|
||||||
|
var today = DateTime.Today;
|
||||||
|
var calendar = CultureInfo.CurrentCulture.Calendar;
|
||||||
|
var weekNumber = calendar.GetWeekOfYear(
|
||||||
|
today,
|
||||||
|
CalendarWeekRule.FirstFourDayWeek,
|
||||||
|
DayOfWeek.Monday
|
||||||
|
);
|
||||||
|
return $"{today.Year}-W{weekNumber:D2}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت شماره هفته قبل
|
||||||
|
/// </summary>
|
||||||
|
private static string GetPreviousWeekNumber()
|
||||||
|
{
|
||||||
|
var lastWeek = DateTime.Today.AddDays(-7);
|
||||||
|
var calendar = CultureInfo.CurrentCulture.Calendar;
|
||||||
|
var weekNumber = calendar.GetWeekOfYear(
|
||||||
|
lastWeek,
|
||||||
|
CalendarWeekRule.FirstFourDayWeek,
|
||||||
|
DayOfWeek.Monday
|
||||||
|
);
|
||||||
|
return $"{lastWeek.Year}-W{weekNumber:D2}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
_timer?.Dispose();
|
||||||
|
base.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Infrastructure.Persistence;
|
using CMSMicroservice.Infrastructure.Persistence;
|
||||||
using CMSMicroservice.Infrastructure.Persistence.Interceptors;
|
using CMSMicroservice.Infrastructure.Persistence.Interceptors;
|
||||||
|
using CMSMicroservice.Infrastructure.BackgroundJobs;
|
||||||
|
using CMSMicroservice.Infrastructure.Services.Monitoring;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
@@ -20,7 +22,14 @@ public static class ConfigureServices
|
|||||||
services.AddScoped<ApplicationDbContextInitialiser>();
|
services.AddScoped<ApplicationDbContextInitialiser>();
|
||||||
services.AddScoped<IGenerateJwtToken, GenerateJwtTokenService>();
|
services.AddScoped<IGenerateJwtToken, GenerateJwtTokenService>();
|
||||||
services.AddScoped<IHashService, HashService>();
|
services.AddScoped<IHashService, HashService>();
|
||||||
|
services.AddScoped<INetworkPlacementService, NetworkPlacementService>();
|
||||||
|
services.AddScoped<IAlertService, AlertService>();
|
||||||
|
services.AddScoped<IUserNotificationService, UserNotificationService>();
|
||||||
services.AddScoped<IApplicationDbContext>(p => p.GetRequiredService<ApplicationDbContext>());
|
services.AddScoped<IApplicationDbContext>(p => p.GetRequiredService<ApplicationDbContext>());
|
||||||
|
|
||||||
|
// Background Workers
|
||||||
|
services.AddHostedService<WeeklyNetworkCommissionWorker>();
|
||||||
|
|
||||||
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
|
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
|
||||||
{
|
{
|
||||||
services.AddDbContext<ApplicationDbContext>(options =>
|
services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using CMSMicroservice.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Data.Seeding;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seeder for migrating existing User.ParentId to User.NetworkParentId
|
||||||
|
/// این Seeder فقط یک بار اجرا میشود و دادههای قدیمی را به ساختار Binary Tree جدید منتقل میکند
|
||||||
|
/// </summary>
|
||||||
|
public class NetworkParentIdMigrationSeeder
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
private readonly ILogger<NetworkParentIdMigrationSeeder> _logger;
|
||||||
|
|
||||||
|
public NetworkParentIdMigrationSeeder(
|
||||||
|
ApplicationDbContext context,
|
||||||
|
ILogger<NetworkParentIdMigrationSeeder> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SeedAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("=== Starting ParentId → NetworkParentId Migration ===");
|
||||||
|
|
||||||
|
// Step 1: Validation - Check if migration already done
|
||||||
|
var alreadyMigrated = await _context.Users
|
||||||
|
.Where(u => u.ParentId != null && u.NetworkParentId != null)
|
||||||
|
.AnyAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (alreadyMigrated)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("⚠️ Migration already completed! Skipping...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Find users with ParentId but no NetworkParentId
|
||||||
|
var usersToMigrate = await _context.Users
|
||||||
|
.Where(u => u.ParentId != null && u.NetworkParentId == null)
|
||||||
|
.OrderBy(u => u.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (usersToMigrate.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("✅ No users to migrate. All done!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation($"📊 Found {usersToMigrate.Count} users to migrate");
|
||||||
|
|
||||||
|
// Step 3: Group by ParentId to check binary tree constraint
|
||||||
|
var parentGroups = usersToMigrate.GroupBy(u => u.ParentId);
|
||||||
|
|
||||||
|
int migratedCount = 0;
|
||||||
|
int skippedCount = 0;
|
||||||
|
|
||||||
|
foreach (var group in parentGroups)
|
||||||
|
{
|
||||||
|
var parentId = group.Key;
|
||||||
|
var children = group.OrderBy(u => u.Id).ToList(); // ترتیب بر اساس Id
|
||||||
|
|
||||||
|
if (children.Count > 2)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"⚠️ Parent {ParentId} has {Count} children! Binary tree allows max 2. Taking first 2...",
|
||||||
|
parentId, children.Count);
|
||||||
|
|
||||||
|
children = children.Take(2).ToList();
|
||||||
|
skippedCount += (group.Count() - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign NetworkParentId and LegPosition
|
||||||
|
for (int i = 0; i < children.Count && i < 2; i++)
|
||||||
|
{
|
||||||
|
var child = children[i];
|
||||||
|
child.NetworkParentId = parentId;
|
||||||
|
child.LegPosition = i == 0 ? NetworkLeg.Left : NetworkLeg.Right;
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"✅ Migrated User {UserId}: Parent={ParentId}, Leg={Leg}",
|
||||||
|
child.Id, parentId, child.LegPosition);
|
||||||
|
|
||||||
|
migratedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Save changes
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"✅ Migration Completed! Migrated={Migrated}, Skipped={Skipped}",
|
||||||
|
migratedCount, skippedCount);
|
||||||
|
|
||||||
|
// Step 5: Post-Migration Validation
|
||||||
|
await ValidateMigrationAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidateMigrationAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("🔍 Validating Migration...");
|
||||||
|
|
||||||
|
// Check 1: Orphaned nodes (NetworkParent doesn't exist)
|
||||||
|
var orphanedUsers = await _context.Users
|
||||||
|
.Where(u => u.NetworkParentId != null &&
|
||||||
|
!_context.Users.Any(p => p.Id == u.NetworkParentId))
|
||||||
|
.Select(u => new { u.Id, u.NetworkParentId })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (orphanedUsers.Any())
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"❌ Found {Count} orphaned users (NetworkParent doesn't exist): {Ids}",
|
||||||
|
orphanedUsers.Count,
|
||||||
|
string.Join(", ", orphanedUsers.Select(u => u.Id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 2: Binary tree violation (more than 2 children per parent)
|
||||||
|
var parentsWithTooManyChildren = await _context.Users
|
||||||
|
.Where(u => u.NetworkParentId != null)
|
||||||
|
.GroupBy(u => u.NetworkParentId)
|
||||||
|
.Select(g => new { ParentId = g.Key, Count = g.Count() })
|
||||||
|
.Where(x => x.Count > 2)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (parentsWithTooManyChildren.Any())
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"❌ Binary tree violation! {Count} parents have more than 2 children",
|
||||||
|
parentsWithTooManyChildren.Count);
|
||||||
|
|
||||||
|
foreach (var parent in parentsWithTooManyChildren)
|
||||||
|
{
|
||||||
|
_logger.LogError(" Parent {ParentId} has {Count} children", parent.ParentId, parent.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 3: Statistics
|
||||||
|
var stats = await _context.Users
|
||||||
|
.GroupBy(u => 1)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
TotalUsers = g.Count(),
|
||||||
|
UsersWithNetworkParent = g.Count(u => u.NetworkParentId != null),
|
||||||
|
LeftChildren = g.Count(u => u.LegPosition == NetworkLeg.Left),
|
||||||
|
RightChildren = g.Count(u => u.LegPosition == NetworkLeg.Right)
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (stats != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("📊 Migration Statistics:");
|
||||||
|
_logger.LogInformation(" Total Users: {Total}", stats.TotalUsers);
|
||||||
|
_logger.LogInformation(" Users with NetworkParent: {Count}", stats.UsersWithNetworkParent);
|
||||||
|
_logger.LogInformation(" Left Children: {Count}", stats.LeftChildren);
|
||||||
|
_logger.LogInformation(" Right Children: {Count}", stats.RightChildren);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orphanedUsers.Any() && !parentsWithTooManyChildren.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInformation("✅ Validation Passed! Binary tree is intact.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError("❌ Validation Failed! Please fix issues manually.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- Migration Script: ParentId → NetworkParentId & LegPosition Assignment
|
||||||
|
-- Date: 2025-06-01
|
||||||
|
-- Purpose: Migrate existing User.ParentId data to new NetworkParentId + LegPosition binary tree structure
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Step 1: Validation - Find users with more than 2 children (INVALID for binary tree)
|
||||||
|
-- این کاربران باید قبل از Migration بررسی شوند
|
||||||
|
SELECT
|
||||||
|
ParentId,
|
||||||
|
COUNT(*) as ChildCount,
|
||||||
|
STRING_AGG(CAST(Id AS VARCHAR), ', ') as ChildIds
|
||||||
|
FROM Users
|
||||||
|
WHERE ParentId IS NOT NULL
|
||||||
|
GROUP BY ParentId
|
||||||
|
HAVING COUNT(*) > 2;
|
||||||
|
|
||||||
|
-- اگر نتیجهای بود، باید دستی تصمیم بگیرید کدام 2 فرزند باقی بمانند!
|
||||||
|
-- اگر نتیجهای نبود، ادامه دهید:
|
||||||
|
|
||||||
|
-- Step 2: Copy ParentId → NetworkParentId for all users
|
||||||
|
UPDATE Users
|
||||||
|
SET NetworkParentId = ParentId
|
||||||
|
WHERE ParentId IS NOT NULL
|
||||||
|
AND NetworkParentId IS NULL;
|
||||||
|
|
||||||
|
-- Step 3: Assign LegPosition (Left/Right) based on order
|
||||||
|
-- برای هر Parent، اولین فرزند = Left، دومین فرزند = Right
|
||||||
|
WITH RankedChildren AS (
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
ParentId,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY ParentId ORDER BY Id ASC) as ChildRank
|
||||||
|
FROM Users
|
||||||
|
WHERE ParentId IS NOT NULL
|
||||||
|
)
|
||||||
|
UPDATE Users
|
||||||
|
SET LegPosition = CASE
|
||||||
|
WHEN rc.ChildRank = 1 THEN 0 -- Left = 0 (enum value)
|
||||||
|
WHEN rc.ChildRank = 2 THEN 1 -- Right = 1 (enum value)
|
||||||
|
ELSE NULL -- اگر بیشتر از 2 فرزند بود (نباید اتفاق بیفته)
|
||||||
|
END
|
||||||
|
FROM Users u
|
||||||
|
INNER JOIN RankedChildren rc ON u.Id = rc.Id;
|
||||||
|
|
||||||
|
-- Step 4: Validation - Check for orphaned nodes (Parent doesn't exist)
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
NetworkParentId,
|
||||||
|
'Orphaned: Parent does not exist' as Issue
|
||||||
|
FROM Users
|
||||||
|
WHERE NetworkParentId IS NOT NULL
|
||||||
|
AND NetworkParentId NOT IN (SELECT Id FROM Users);
|
||||||
|
|
||||||
|
-- اگر Orphan یافت شد، باید آنها را NULL کنید یا Parent صحیح تخصیص دهید
|
||||||
|
|
||||||
|
-- Step 5: Validation - Verify binary tree integrity
|
||||||
|
-- هر Parent باید حداکثر 2 فرزند داشته باشد
|
||||||
|
SELECT
|
||||||
|
NetworkParentId,
|
||||||
|
COUNT(*) as ChildCount,
|
||||||
|
STRING_AGG(CAST(Id AS VARCHAR), ', ') as ChildIds
|
||||||
|
FROM Users
|
||||||
|
WHERE NetworkParentId IS NOT NULL
|
||||||
|
GROUP BY NetworkParentId
|
||||||
|
HAVING COUNT(*) > 2;
|
||||||
|
|
||||||
|
-- اگر نتیجه خالی بود، Migration موفق است!
|
||||||
|
|
||||||
|
-- Step 6: Statistics
|
||||||
|
SELECT
|
||||||
|
'Total Users' as Metric,
|
||||||
|
COUNT(*) as Count
|
||||||
|
FROM Users
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'Users with NetworkParentId',
|
||||||
|
COUNT(*)
|
||||||
|
FROM Users
|
||||||
|
WHERE NetworkParentId IS NOT NULL
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'Users with LegPosition Left',
|
||||||
|
COUNT(*)
|
||||||
|
FROM Users
|
||||||
|
WHERE LegPosition = 0
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'Users with LegPosition Right',
|
||||||
|
COUNT(*)
|
||||||
|
FROM Users
|
||||||
|
WHERE LegPosition = 1;
|
||||||
|
|
||||||
|
-- Commit if validation passes
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ROLLBACK; -- اگر مشکل پیش آمد، uncomment کنید
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Services.Monitoring;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پیادهسازی اولیه AlertService
|
||||||
|
/// TODO: Integration با Sentry, Slack, Email
|
||||||
|
/// </summary>
|
||||||
|
public class AlertService : IAlertService
|
||||||
|
{
|
||||||
|
private readonly ILogger<AlertService> _logger;
|
||||||
|
|
||||||
|
public AlertService(ILogger<AlertService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendCriticalAlertAsync(
|
||||||
|
string title,
|
||||||
|
string message,
|
||||||
|
Exception? exception = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogCritical(exception, "🚨 CRITICAL ALERT: {Title} - {Message}", title, message);
|
||||||
|
|
||||||
|
// TODO: Integration
|
||||||
|
// - Send to Sentry
|
||||||
|
// - Send to Slack
|
||||||
|
// - Send Email to Admins
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendWarningAlertAsync(
|
||||||
|
string title,
|
||||||
|
string message,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("⚠️ WARNING ALERT: {Title} - {Message}", title, message);
|
||||||
|
|
||||||
|
// TODO: Integration
|
||||||
|
// - Send to Slack
|
||||||
|
// - Log to monitoring system
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendSuccessNotificationAsync(
|
||||||
|
string title,
|
||||||
|
string message,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("✅ SUCCESS: {Title} - {Message}", title, message);
|
||||||
|
|
||||||
|
// TODO: Optional Slack notification for important success events
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Services.Monitoring;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تنظیمات Monitoring و Alerting
|
||||||
|
/// در appsettings.json تعریف میشود
|
||||||
|
/// </summary>
|
||||||
|
public class MonitoringSettings
|
||||||
|
{
|
||||||
|
public const string SectionName = "Monitoring";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فعال بودن Sentry
|
||||||
|
/// </summary>
|
||||||
|
public bool SentryEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sentry DSN
|
||||||
|
/// </summary>
|
||||||
|
public string? SentryDsn { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فعال بودن Slack Notifications
|
||||||
|
/// </summary>
|
||||||
|
public bool SlackEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Slack Webhook URL
|
||||||
|
/// </summary>
|
||||||
|
public string? SlackWebhookUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فعال بودن Email Alerts
|
||||||
|
/// </summary>
|
||||||
|
public bool EmailAlertsEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// لیست ایمیلهای Admin برای دریافت Alert
|
||||||
|
/// </summary>
|
||||||
|
public List<string> AdminEmails { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فعال بودن SMS Notifications به کاربران
|
||||||
|
/// </summary>
|
||||||
|
public bool SmsNotificationsEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SMS Gateway API Key
|
||||||
|
/// </summary>
|
||||||
|
public string? SmsApiKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SMS Gateway Base URL
|
||||||
|
/// </summary>
|
||||||
|
public string? SmsGatewayUrl { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Services.Monitoring;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پیادهسازی اولیه UserNotificationService
|
||||||
|
/// TODO: Integration با SMS Gateway, Email Service, Push Notification
|
||||||
|
/// </summary>
|
||||||
|
public class UserNotificationService : IUserNotificationService
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ILogger<UserNotificationService> _logger;
|
||||||
|
|
||||||
|
public UserNotificationService(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
ILogger<UserNotificationService> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendCommissionReceivedNotificationAsync(
|
||||||
|
long userId,
|
||||||
|
decimal amount,
|
||||||
|
int weekNumber,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"📧 Sending commission notification: User={UserId}, Amount={Amount}, Week={WeekNumber}",
|
||||||
|
userId, amount, weekNumber);
|
||||||
|
|
||||||
|
// TODO: Implementation
|
||||||
|
// 1. Get User preferences (SMS/Email/Push enabled?)
|
||||||
|
// 2. Send SMS via SMS Gateway
|
||||||
|
// 3. Send Email via Email Service
|
||||||
|
// 4. Send Push Notification
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendClubActivationNotificationAsync(
|
||||||
|
long userId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("🎉 Sending club activation notification: User={UserId}", userId);
|
||||||
|
|
||||||
|
// TODO: Implementation
|
||||||
|
// - Welcome message for club membership
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendPayoutErrorNotificationAsync(
|
||||||
|
long userId,
|
||||||
|
string errorMessage,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"⚠️ Sending payout error notification: User={UserId}, Error={Error}",
|
||||||
|
userId, errorMessage);
|
||||||
|
|
||||||
|
// TODO: Implementation
|
||||||
|
// - Notify user about payment failure
|
||||||
|
// - Provide retry instructions
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پیادهسازی سرویس محاسبه موقعیت در Binary Tree
|
||||||
|
/// </summary>
|
||||||
|
public class NetworkPlacementService : INetworkPlacementService
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ILogger<NetworkPlacementService> _logger;
|
||||||
|
|
||||||
|
public NetworkPlacementService(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
ILogger<NetworkPlacementService> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<NetworkLeg?> CalculateLegPositionAsync(long parentId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// بررسی وجود Parent
|
||||||
|
var parentExists = await _context.Users.AnyAsync(u => u.Id == parentId, cancellationToken);
|
||||||
|
if (!parentExists)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Parent {ParentId} does not exist", parentId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// شمارش فرزندان فعلی
|
||||||
|
var children = await _context.Users
|
||||||
|
.Where(u => u.NetworkParentId == parentId)
|
||||||
|
.Select(u => new { u.LegPosition })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (children.Count >= 2)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Parent {ParentId} already has 2 children. Binary Tree is full!", parentId);
|
||||||
|
return null; // Binary Tree پر است
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی کدام Leg خالی است
|
||||||
|
var hasLeft = children.Any(c => c.LegPosition == NetworkLeg.Left);
|
||||||
|
var hasRight = children.Any(c => c.LegPosition == NetworkLeg.Right);
|
||||||
|
|
||||||
|
if (!hasLeft)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Parent {ParentId}: Left leg is available", parentId);
|
||||||
|
return NetworkLeg.Left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasRight)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Parent {ParentId}: Right leg is available", parentId);
|
||||||
|
return NetworkLeg.Right;
|
||||||
|
}
|
||||||
|
|
||||||
|
// نباید به اینجا برسیم (چون Count < 2 بود)
|
||||||
|
_logger.LogError("Unexpected state: Parent {ParentId} has {Count} children but no available leg",
|
||||||
|
parentId, children.Count);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanAcceptChildAsync(long parentId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var childCount = await _context.Users
|
||||||
|
.CountAsync(u => u.NetworkParentId == parentId, cancellationToken);
|
||||||
|
|
||||||
|
return childCount < 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long?> FindAvailableParentAsync(long rootParentId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// BFS (Breadth-First Search) برای پیدا کردن اولین Parent با جای خالی
|
||||||
|
var queue = new Queue<long>();
|
||||||
|
queue.Enqueue(rootParentId);
|
||||||
|
var visited = new HashSet<long>();
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var currentParentId = queue.Dequeue();
|
||||||
|
|
||||||
|
if (visited.Contains(currentParentId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
visited.Add(currentParentId);
|
||||||
|
|
||||||
|
// بررسی کنید که آیا این Parent میتواند فرزند بپذیرد
|
||||||
|
var canAccept = await CanAcceptChildAsync(currentParentId, cancellationToken);
|
||||||
|
if (canAccept)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Found available parent: {ParentId}", currentParentId);
|
||||||
|
return currentParentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// اضافه کردن فرزندان به صف برای جستجو
|
||||||
|
var children = await _context.Users
|
||||||
|
.Where(u => u.NetworkParentId == currentParentId)
|
||||||
|
.Select(u => u.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
foreach (var childId in children)
|
||||||
|
{
|
||||||
|
queue.Enqueue(childId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogWarning("No available parent found in network starting from {RootParentId}", rootParentId);
|
||||||
|
return null; // هیچ Parent خالی پیدا نشد
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>0.0.139</Version>
|
<Version>0.0.140</Version>
|
||||||
<DebugType>None</DebugType>
|
<DebugType>None</DebugType>
|
||||||
<DebugSymbols>False</DebugSymbols>
|
<DebugSymbols>False</DebugSymbols>
|
||||||
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using CMSMicroservice.Infrastructure.Persistence;
|
using CMSMicroservice.Infrastructure.Persistence;
|
||||||
|
using CMSMicroservice.Infrastructure.Data.Seeding;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Serilog.Core;
|
using Serilog.Core;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@@ -99,6 +101,12 @@ if (app.Environment.IsDevelopment())
|
|||||||
var initialiser = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitialiser>();
|
var initialiser = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitialiser>();
|
||||||
await initialiser.InitialiseAsync();
|
await initialiser.InitialiseAsync();
|
||||||
await initialiser.SeedAsync();
|
await initialiser.SeedAsync();
|
||||||
|
|
||||||
|
// Run Migration: ParentId → NetworkParentId (فقط یکبار اجرا میشود)
|
||||||
|
var migrationLogger = scope.ServiceProvider.GetRequiredService<ILogger<NetworkParentIdMigrationSeeder>>();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||||
|
var migrationSeeder = new NetworkParentIdMigrationSeeder(dbContext, migrationLogger);
|
||||||
|
await migrationSeeder.SeedAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -10,6 +10,19 @@
|
|||||||
"Otp": {
|
"Otp": {
|
||||||
"Secret": "K2w8k1h1mH2Qz1kqWk0c8kQ2Pq8q9H1eE2nqN1qQ8x7M="
|
"Secret": "K2w8k1h1mH2Qz1kqWk0c8kQ2Pq8q9H1eE2nqN1qQ8x7M="
|
||||||
},
|
},
|
||||||
|
"Monitoring": {
|
||||||
|
"SentryEnabled": false,
|
||||||
|
"SentryDsn": "",
|
||||||
|
"SlackEnabled": false,
|
||||||
|
"SlackWebhookUrl": "",
|
||||||
|
"EmailAlertsEnabled": false,
|
||||||
|
"AdminEmails": [
|
||||||
|
"admin@example.com"
|
||||||
|
],
|
||||||
|
"SmsNotificationsEnabled": false,
|
||||||
|
"SmsApiKey": "",
|
||||||
|
"SmsGatewayUrl": ""
|
||||||
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
"EndpointDefaults": {
|
"EndpointDefaults": {
|
||||||
|
|||||||
Reference in New Issue
Block a user