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; using CMSMicroservice.Domain.Entities.Commission; using Polly; using Polly.Retry; namespace CMSMicroservice.Infrastructure.BackgroundJobs; /// /// Background Worker برای محاسبه و توزیع کمیسیون‌های هفتگی شبکه /// زمان اجرا: هر یکشنبه ساعت 23:59 /// public class WeeklyNetworkCommissionWorker : BackgroundService { private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; private Timer? _timer; private readonly ResiliencePipeline _retryPipeline; public WeeklyNetworkCommissionWorker( ILogger logger, IServiceProvider serviceProvider) { _logger = logger; _serviceProvider = serviceProvider; // ایجاد Retry Policy با Exponential Backoff _retryPipeline = new ResiliencePipelineBuilder() .AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 3, Delay = TimeSpan.FromMinutes(5), BackoffType = DelayBackoffType.Exponential, UseJitter = true, OnRetry = args => { _logger.LogWarning( "Retry attempt {AttemptNumber} after {Delay}ms due to: {Exception}", args.AttemptNumber, args.RetryDelay.TotalMilliseconds, args.Outcome.Exception?.Message); return ValueTask.CompletedTask; } }) .Build(); } 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 برای اجرا در زمان مشخص و تکرار هفتگی با Retry _timer = new Timer( callback: async _ => await _retryPipeline.ExecuteAsync( async ct => await ExecuteWeeklyCalculationAsync(ct), stoppingToken), state: null, dueTime: delay, period: TimeSpan.FromDays(7) // هر 7 روز یکبار ); return Task.CompletedTask; } /// /// محاسبه تاریخ یکشنبه بعدی /// 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); } /// /// اجرای محاسبات هفتگی کمیسیون /// private async Task ExecuteWeeklyCalculationAsync(CancellationToken cancellationToken) { var executionId = Guid.NewGuid(); var startTime = DateTime.Now; _logger.LogInformation("=== Starting Weekly Commission Calculation [{ExecutionId}] at {Time} (UTC) ===", executionId, startTime); WorkerExecutionLog? log = null; try { using var scope = _serviceProvider.CreateScope(); var mediator = scope.ServiceProvider.GetRequiredService(); var context = scope.ServiceProvider.GetRequiredService(); // دریافت شماره هفته قبل (هفته‌ای که باید محاسبه شود) var previousWeekNumber = GetPreviousWeekNumber(); var currentWeekNumber = GetCurrentWeekNumber(); _logger.LogInformation("Processing week: {WeekNumber}", previousWeekNumber); // ایجاد Log log = new WorkerExecutionLog { ExecutionId = executionId, WeekNumber = previousWeekNumber, StartedAt = startTime, Status = WorkerExecutionStatus.Running, ProcessedCount = 0, ErrorCount = 0 }; await context.WorkerExecutionLogs.AddAsync(log, cancellationToken); await context.SaveChangesAsync(cancellationToken); // ===== 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); // Update log log.Status = WorkerExecutionStatus.SuccessWithWarnings; log.CompletedAt = DateTime.Now; log.DurationMs = (long)(log.CompletedAt.Value - log.StartedAt).TotalMilliseconds; log.Details = "Week already calculated - skipped"; await context.SaveChangesAsync(cancellationToken); 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 completedAt = DateTime.Now; var duration = completedAt - startTime; // Update log - Success if (log != null) { log.Status = WorkerExecutionStatus.Success; log.CompletedAt = completedAt; log.DurationMs = (long)duration.TotalMilliseconds; log.ProcessedCount = balancesCalculated + payoutsProcessed; log.Details = $"Success: {balancesCalculated} balances, {payoutsProcessed} payouts, {balancesToExpire.Count} expired"; await context.SaveChangesAsync(cancellationToken); } _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(); 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) { var previousWeekNumber = GetPreviousWeekNumber(); _logger.LogCritical(ex, "!!! CRITICAL ERROR in Weekly Commission Calculation [{ExecutionId}] !!!" + "\n Week: {WeekNumber}" + "\n Message: {Message}" + "\n StackTrace: {StackTrace}" + "\n Please investigate immediately!", executionId, previousWeekNumber, ex.Message, ex.StackTrace); // Update log - Failed if (log != null) { try { using var errorScope = _serviceProvider.CreateScope(); var context = errorScope.ServiceProvider.GetRequiredService(); log.Status = WorkerExecutionStatus.Failed; log.CompletedAt = DateTime.Now; log.DurationMs = (long)(log.CompletedAt.Value - log.StartedAt).TotalMilliseconds; log.ErrorCount = 1; log.ErrorMessage = ex.Message; log.ErrorStackTrace = ex.StackTrace; await context.SaveChangesAsync(cancellationToken); } catch (Exception logEx) { _logger.LogError(logEx, "Failed to update error log"); } } // ===== ERROR HANDLING & ALERTING ===== // در محیط production باید Alert/Notification ارسال شود using var alertScope = _serviceProvider.CreateScope(); var alertService = alertScope.ServiceProvider.GetRequiredService(); await alertService.SendCriticalAlertAsync( "Weekly Commission Worker Failed", $"Worker execution {executionId} failed for week {previousWeekNumber}. Will retry with exponential backoff.", ex, cancellationToken); // Retry با Polly - اگر همچنان fail کند exception throw می‌شود throw; } } /// /// دریافت شماره هفته جاری (فرمت ISO 8601: YYYY-Www) /// 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}"; } /// /// دریافت شماره هفته قبل /// 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(); } }