using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool;
using CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
using CMSMicroservice.Application.CommissionCQ.Commands.TriggerWeeklyCalculation;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Polly;
namespace CMSMicroservice.Infrastructure.BackgroundJobs;
///
/// Hangfire Job for weekly commission calculation
/// Executes every Sunday at 00:05 (Cron: "5 0 * * 0")
///
public class WeeklyCommissionJob
{
private readonly IMediator _mediator;
private readonly ILogger _logger;
private readonly IApplicationDbContext _context;
private readonly IWeekDefinitionRepository _weekRepository;
private readonly ResiliencePipeline _retryPipeline;
public WeeklyCommissionJob(
IMediator mediator,
ILogger logger,
IApplicationDbContext context,
IWeekDefinitionRepository weekRepository)
{
_mediator = mediator;
_logger = logger;
_context = context;
_weekRepository = weekRepository;
// Polly Retry: 3 attempts, exponential backoff (5min → 10min → 20min)
_retryPipeline = new ResiliencePipelineBuilder()
.AddRetry(new Polly.Retry.RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMinutes(5),
BackoffType = Polly.DelayBackoffType.Exponential,
UseJitter = true,
OnRetry = args =>
{
_logger.LogWarning(
"⚠️ Retry attempt {AttemptNumber} after {Delay}ms delay. Exception: {ExceptionType}",
args.AttemptNumber,
args.RetryDelay.TotalMilliseconds,
args.Outcome.Exception?.GetType().Name ?? "None");
return ValueTask.CompletedTask;
}
})
.Build();
}
///
/// Execute weekly commission calculation with retry logic
/// Called by Hangfire scheduler or manually triggered
///
/// شناسه هفته. اگر null باشد، هفته قبلی محاسبه میشود
/// Cancellation token
public async Task ExecuteAsync(long? weekDefinitionId = null, CancellationToken cancellationToken = default)
{
var executionId = Guid.NewGuid();
var startTime = DateTime.Now;
// Use provided WeekDefinitionId or calculate for PREVIOUS week (completed week)
long targetWeekDefinitionId;
if (weekDefinitionId.HasValue && weekDefinitionId.Value > 0)
{
targetWeekDefinitionId = weekDefinitionId.Value;
_logger.LogInformation("📅 Using manually specified WeekDefinitionId: {WeekDefinitionId}", targetWeekDefinitionId);
}
else
{
var previousWeek = DateTime.Now.AddDays(-7);
var weekDef = _weekRepository.GetWeekByDate(previousWeek);
if (weekDef == null)
{
throw new InvalidOperationException($"هفته برای تاریخ {previousWeek:yyyy-MM-dd} تعریف نشده است");
}
targetWeekDefinitionId = weekDef.Id;
_logger.LogInformation("📅 Using previous week (auto-calculated): WeekDefinitionId={WeekDefinitionId}, WeekNumber={WeekNumber}",
targetWeekDefinitionId, weekDef.GregorianWeekNumber);
}
_logger.LogInformation(
"🚀 [{ExecutionId}] Starting weekly commission calculation for WeekDefinitionId={WeekDefinitionId}",
executionId, targetWeekDefinitionId);
// Create execution log entry
var log = new WorkerExecutionLog
{
ExecutionId = executionId,
WeekDefinitionId = targetWeekDefinitionId,
StartedAt = startTime,
Status = WorkerExecutionStatus.Running
};
_context.WorkerExecutionLogs.Add(log);
await _context.SaveChangesAsync(cancellationToken);
try
{
// Execute with retry pipeline
await _retryPipeline.ExecuteAsync(async ct =>
{
await ExecuteWeeklyCalculationAsync(executionId, targetWeekDefinitionId, ct);
}, cancellationToken);
// Update log on success
var completedAt = DateTime.Now;
var duration = completedAt - startTime;
log.Status = WorkerExecutionStatus.Success;
log.CompletedAt = completedAt;
log.DurationMs = (long)duration.TotalMilliseconds;
// Get counts from database
var balancesCount = await _context.NetworkWeeklyBalances
.CountAsync(x => x.WeekDefinitionId == targetWeekDefinitionId, cancellationToken);
var payoutsCount = await _context.UserCommissionPayouts
.CountAsync(x => x.WeekDefinitionId == targetWeekDefinitionId, cancellationToken);
log.ProcessedCount = balancesCount + payoutsCount;
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"✅ [{ExecutionId}] Completed successfully in {Duration}s | Balances: {BalancesCount}, Payouts: {PayoutsCount}",
executionId, duration.TotalSeconds, balancesCount, payoutsCount);
}
catch (Exception ex)
{
// Update log on failure
var completedAt = DateTime.Now;
var duration = completedAt - startTime;
log.Status = WorkerExecutionStatus.Failed;
log.CompletedAt = completedAt;
log.DurationMs = (long)duration.TotalMilliseconds;
log.ErrorMessage = ex.Message;
log.ErrorStackTrace = ex.StackTrace;
await _context.SaveChangesAsync(cancellationToken);
_logger.LogError(ex,
"❌ [{ExecutionId}] Failed after {Duration}s: {ErrorMessage}",
executionId, duration.TotalSeconds, ex.Message);
throw; // Re-throw for Hangfire to mark job as failed
}
}
private async Task ExecuteWeeklyCalculationAsync(
Guid executionId,
long weekDefinitionId,
CancellationToken cancellationToken)
{
// Check idempotency: Skip if already calculated
var existingPool = await _context.WeeklyCommissionPools
.FirstOrDefaultAsync(x => x.WeekDefinitionId == weekDefinitionId, cancellationToken);
if (existingPool != null && existingPool.IsCalculated)
{
_logger.LogWarning(
"⚠️ [{ExecutionId}] WeekDefinitionId={WeekDefinitionId} already calculated. Skipping.",
executionId, weekDefinitionId);
return;
}
// دریافت WeekNumber برای command ها (فعلاً هنوز از WeekNumber استفاده میکنند)
var GregorianWeekNumber = _weekRepository.GetGregorianWeekNumber(weekDefinitionId);
if (string.IsNullOrEmpty(GregorianWeekNumber))
{
throw new InvalidOperationException($"WeekDefinitionId={weekDefinitionId} یافت نشد");
}
using var transaction = new System.Transactions.TransactionScope(
System.Transactions.TransactionScopeOption.Required,
new System.Transactions.TransactionOptions
{
IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted,
Timeout = TimeSpan.FromMinutes(30)
},
System.Transactions.TransactionScopeAsyncFlowOption.Enabled);
try
{
// Step 1: Calculate user balances (Left/Right leg volumes)
_logger.LogInformation(
"📊 [{ExecutionId}] Step 1/3: Calculating weekly balances...",
executionId);
await _mediator.Send(new TriggerWeeklyCalculationCommand
{
WeekDefinitionId = weekDefinitionId,
ForceRecalculate = false
}, cancellationToken);
transaction.Complete();
_logger.LogInformation(
"✅ [{ExecutionId}] All steps completed successfully",
executionId);
}
catch (Exception ex)
{
_logger.LogError(ex,
"❌ [{ExecutionId}] Transaction rolled back: {ErrorMessage}",
executionId, ex.Message);
throw;
}
}
}