feat: Add monitoring alerts skeleton and enhance worker with notifications

This commit is contained in:
masoodafar-web
2025-11-30 20:18:10 +03:30
parent 55fa71e09b
commit 199e7e99d1
23 changed files with 5038 additions and 1168 deletions

View File

@@ -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();
}
}

View File

@@ -1,6 +1,8 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Infrastructure.Persistence;
using CMSMicroservice.Infrastructure.Persistence.Interceptors;
using CMSMicroservice.Infrastructure.BackgroundJobs;
using CMSMicroservice.Infrastructure.Services.Monitoring;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -20,7 +22,14 @@ public static class ConfigureServices
services.AddScoped<ApplicationDbContextInitialiser>();
services.AddScoped<IGenerateJwtToken, GenerateJwtTokenService>();
services.AddScoped<IHashService, HashService>();
services.AddScoped<INetworkPlacementService, NetworkPlacementService>();
services.AddScoped<IAlertService, AlertService>();
services.AddScoped<IUserNotificationService, UserNotificationService>();
services.AddScoped<IApplicationDbContext>(p => p.GetRequiredService<ApplicationDbContext>());
// Background Workers
services.AddHostedService<WeeklyNetworkCommissionWorker>();
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
{
services.AddDbContext<ApplicationDbContext>(options =>

View File

@@ -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.");
}
}
}

View File

@@ -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 کنید

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

@@ -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 خالی پیدا نشد
}
}