feat: Enhance network membership and withdrawal processing with user tracking and logging
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
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;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Polly;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.BackgroundJobs;
|
||||
|
||||
/// <summary>
|
||||
/// Hangfire Job for weekly commission calculation
|
||||
/// Executes every Sunday at 00:05 (Cron: "5 0 * * 0")
|
||||
/// </summary>
|
||||
public class WeeklyCommissionJob
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<WeeklyCommissionJob> _logger;
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ResiliencePipeline _retryPipeline;
|
||||
|
||||
public WeeklyCommissionJob(
|
||||
IMediator mediator,
|
||||
ILogger<WeeklyCommissionJob> logger,
|
||||
IApplicationDbContext context)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
_context = context;
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute weekly commission calculation with retry logic
|
||||
/// Called by Hangfire scheduler
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var executionId = Guid.NewGuid();
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
// Calculate for PREVIOUS week (completed week)
|
||||
var previousWeek = DateTime.UtcNow.AddDays(-7);
|
||||
var previousWeekNumber = GetWeekNumber(previousWeek);
|
||||
|
||||
_logger.LogInformation(
|
||||
"🚀 [{ExecutionId}] Starting weekly commission calculation for {WeekNumber}",
|
||||
executionId, previousWeekNumber);
|
||||
|
||||
// Create execution log entry
|
||||
var log = new WorkerExecutionLog
|
||||
{
|
||||
ExecutionId = executionId,
|
||||
WeekNumber = previousWeekNumber,
|
||||
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, previousWeekNumber, ct);
|
||||
}, cancellationToken);
|
||||
|
||||
// Update log on success
|
||||
var completedAt = DateTime.UtcNow;
|
||||
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.WeekNumber == previousWeekNumber, cancellationToken);
|
||||
var payoutsCount = await _context.UserCommissionPayouts
|
||||
.CountAsync(x => x.WeekNumber == previousWeekNumber, 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.UtcNow;
|
||||
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,
|
||||
string weekNumber,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Check idempotency: Skip if already calculated
|
||||
var existingPool = await _context.WeeklyCommissionPools
|
||||
.FirstOrDefaultAsync(x => x.WeekNumber == weekNumber, cancellationToken);
|
||||
|
||||
if (existingPool != null && existingPool.IsCalculated)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"⚠️ [{ExecutionId}] Week {WeekNumber} already calculated. Skipping.",
|
||||
executionId, weekNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
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 CalculateWeeklyBalancesCommand
|
||||
{
|
||||
WeekNumber = weekNumber,
|
||||
ForceRecalculate = false
|
||||
}, cancellationToken);
|
||||
|
||||
// Step 2: Calculate global commission pool
|
||||
_logger.LogInformation(
|
||||
"💰 [{ExecutionId}] Step 2/3: Calculating commission pool...",
|
||||
executionId);
|
||||
|
||||
await _mediator.Send(new CalculateWeeklyCommissionPoolCommand
|
||||
{
|
||||
WeekNumber = weekNumber,
|
||||
ForceRecalculate = false
|
||||
}, cancellationToken);
|
||||
|
||||
// Step 3: Distribute commissions to users
|
||||
_logger.LogInformation(
|
||||
"💸 [{ExecutionId}] Step 3/3: Processing user payouts...",
|
||||
executionId);
|
||||
|
||||
await _mediator.Send(new ProcessUserPayoutsCommand
|
||||
{
|
||||
WeekNumber = weekNumber,
|
||||
ForceReprocess = false
|
||||
}, cancellationToken);
|
||||
|
||||
transaction.Complete();
|
||||
|
||||
_logger.LogInformation(
|
||||
"✅ [{ExecutionId}] All 3 steps completed successfully",
|
||||
executionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"❌ [{ExecutionId}] Transaction rolled back: {ErrorMessage}",
|
||||
executionId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get ISO 8601 week number (YYYY-Www format)
|
||||
/// </summary>
|
||||
private static string GetWeekNumber(DateTime date)
|
||||
{
|
||||
var calendar = System.Globalization.CultureInfo.InvariantCulture.Calendar;
|
||||
var weekNumber = calendar.GetWeekOfYear(
|
||||
date,
|
||||
System.Globalization.CalendarWeekRule.FirstFourDayWeek,
|
||||
DayOfWeek.Monday);
|
||||
|
||||
var year = date.Year;
|
||||
if (weekNumber >= 52 && date.Month == 1)
|
||||
year--;
|
||||
else if (weekNumber == 1 && date.Month == 12)
|
||||
year++;
|
||||
|
||||
return $"{year}-W{weekNumber:D2}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user