feat: add IsActive field to UserClubFeatures for admin management

This commit is contained in:
masoodafar-web
2025-12-12 01:40:26 +03:30
parent aa66ca10c8
commit ff1c1d5d61
68 changed files with 11456 additions and 198 deletions
@@ -1,6 +1,7 @@
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;
@@ -54,16 +55,30 @@ public class WeeklyCommissionJob
/// <summary>
/// Execute weekly commission calculation with retry logic
/// Called by Hangfire scheduler
/// Called by Hangfire scheduler or manually triggered
/// </summary>
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
/// <param name="weekNumber">Week number in YYYY-Www format (e.g., 2025-W48). If null, uses previous week.</param>
/// <param name="cancellationToken">Cancellation token</param>
public async Task ExecuteAsync(string? weekNumber = null, CancellationToken cancellationToken = default)
{
var executionId = Guid.NewGuid();
var startTime = DateTime.UtcNow;
var startTime = DateTime.Now;
// Calculate for PREVIOUS week (completed week)
var previousWeek = DateTime.UtcNow.AddDays(-7);
var previousWeekNumber = GetWeekNumber(previousWeek);
// Use provided week number or calculate for PREVIOUS week (completed week)
string targetWeekNumber;
if (!string.IsNullOrWhiteSpace(weekNumber))
{
targetWeekNumber = weekNumber;
_logger.LogInformation("📅 Using manually specified week: {WeekNumber}", targetWeekNumber);
}
else
{
var previousWeek = DateTime.Now.AddDays(-7);
targetWeekNumber = GetWeekNumber(previousWeek);
_logger.LogInformation("📅 Using previous week (auto-calculated): {WeekNumber}", targetWeekNumber);
}
var previousWeekNumber = targetWeekNumber;
_logger.LogInformation(
"🚀 [{ExecutionId}] Starting weekly commission calculation for {WeekNumber}",
@@ -89,7 +104,7 @@ public class WeeklyCommissionJob
}, cancellationToken);
// Update log on success
var completedAt = DateTime.UtcNow;
var completedAt = DateTime.Now;
var duration = completedAt - startTime;
log.Status = WorkerExecutionStatus.Success;
@@ -113,7 +128,7 @@ public class WeeklyCommissionJob
catch (Exception ex)
{
// Update log on failure
var completedAt = DateTime.UtcNow;
var completedAt = DateTime.Now;
var duration = completedAt - startTime;
log.Status = WorkerExecutionStatus.Failed;
@@ -165,38 +180,43 @@ public class WeeklyCommissionJob
"📊 [{ExecutionId}] Step 1/3: Calculating weekly balances...",
executionId);
await _mediator.Send(new CalculateWeeklyBalancesCommand
await _mediator.Send(new TriggerWeeklyCalculationCommand
{
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);
// 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}] All 2 steps completed successfully",
executionId);
}
catch (Exception ex)
@@ -109,7 +109,7 @@ public class WeeklyNetworkCommissionWorker : BackgroundService
private async Task ExecuteWeeklyCalculationAsync(CancellationToken cancellationToken)
{
var executionId = Guid.NewGuid();
var startTime = DateTime.UtcNow;
var startTime = DateTime.Now;
_logger.LogInformation("=== Starting Weekly Commission Calculation [{ExecutionId}] at {Time} (UTC) ===",
executionId, startTime);
@@ -154,7 +154,7 @@ public class WeeklyNetworkCommissionWorker : BackgroundService
// Update log
log.Status = WorkerExecutionStatus.SuccessWithWarnings;
log.CompletedAt = DateTime.UtcNow;
log.CompletedAt = DateTime.Now;
log.DurationMs = (long)(log.CompletedAt.Value - log.StartedAt).TotalMilliseconds;
log.Details = "Week already calculated - skipped";
await context.SaveChangesAsync(cancellationToken);
@@ -222,7 +222,7 @@ public class WeeklyNetworkCommissionWorker : BackgroundService
// Commit Transaction
transaction.Complete();
var completedAt = DateTime.UtcNow;
var completedAt = DateTime.Now;
var duration = completedAt - startTime;
// Update log - Success
@@ -297,7 +297,7 @@ public class WeeklyNetworkCommissionWorker : BackgroundService
var context = errorScope.ServiceProvider.GetRequiredService<IApplicationDbContext>();
log.Status = WorkerExecutionStatus.Failed;
log.CompletedAt = DateTime.UtcNow;
log.CompletedAt = DateTime.Now;
log.DurationMs = (long)(log.CompletedAt.Value - log.StartedAt).TotalMilliseconds;
log.ErrorCount = 1;
log.ErrorMessage = ex.Message;
@@ -6,13 +6,13 @@
BEGIN TRANSACTION;
-- Step 1: Validation - Find users with more than 2 children (INVALID for binary tree)
-- Step 1: Validation - Find CMS.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
FROM CMS.Users
WHERE ParentId IS NOT NULL
GROUP BY ParentId
HAVING COUNT(*) > 2;
@@ -20,8 +20,8 @@ HAVING COUNT(*) > 2;
-- اگر نتیجه‌ای بود، باید دستی تصمیم بگیرید کدام 2 فرزند باقی بمانند!
-- اگر نتیجه‌ای نبود، ادامه دهید:
-- Step 2: Copy ParentId → NetworkParentId for all users
UPDATE Users
-- Step 2: Copy ParentId → NetworkParentId for all CMS.Users
UPDATE CMS.Users
SET NetworkParentId = ParentId
WHERE ParentId IS NOT NULL
AND NetworkParentId IS NULL;
@@ -33,16 +33,16 @@ WITH RankedChildren AS (
Id,
ParentId,
ROW_NUMBER() OVER (PARTITION BY ParentId ORDER BY Id ASC) as ChildRank
FROM Users
FROM CMS.Users
WHERE ParentId IS NOT NULL
)
UPDATE Users
UPDATE CMS.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
FROM CMS.Users u
INNER JOIN RankedChildren rc ON u.Id = rc.Id;
-- Step 4: Validation - Check for orphaned nodes (Parent doesn't exist)
@@ -50,9 +50,9 @@ SELECT
Id,
NetworkParentId,
'Orphaned: Parent does not exist' as Issue
FROM Users
FROM CMS.Users
WHERE NetworkParentId IS NOT NULL
AND NetworkParentId NOT IN (SELECT Id FROM Users);
AND NetworkParentId NOT IN (SELECT Id FROM CMS.Users);
-- اگر Orphan یافت شد، باید آنها را NULL کنید یا Parent صحیح تخصیص دهید
@@ -62,7 +62,7 @@ SELECT
NetworkParentId,
COUNT(*) as ChildCount,
STRING_AGG(CAST(Id AS VARCHAR), ', ') as ChildIds
FROM Users
FROM CMS.Users
WHERE NetworkParentId IS NOT NULL
GROUP BY NetworkParentId
HAVING COUNT(*) > 2;
@@ -71,26 +71,26 @@ HAVING COUNT(*) > 2;
-- Step 6: Statistics
SELECT
'Total Users' as Metric,
'Total CMS.Users' as Metric,
COUNT(*) as Count
FROM Users
FROM CMS.Users
UNION ALL
SELECT
'Users with NetworkParentId',
'CMS.Users with NetworkParentId',
COUNT(*)
FROM Users
FROM CMS.Users
WHERE NetworkParentId IS NOT NULL
UNION ALL
SELECT
'Users with LegPosition Left',
'CMS.Users with LegPosition Left',
COUNT(*)
FROM Users
FROM CMS.Users
WHERE LegPosition = 0
UNION ALL
SELECT
'Users with LegPosition Right',
'CMS.Users with LegPosition Right',
COUNT(*)
FROM Users
FROM CMS.Users
WHERE LegPosition = 1;
-- Commit if validation passes
@@ -20,6 +20,7 @@ public class UserClubFeatureConfiguration : IEntityTypeConfiguration<UserClubFea
builder.Property(entity => entity.ClubMembershipId).IsRequired();
builder.Property(entity => entity.ClubFeatureId).IsRequired();
builder.Property(entity => entity.GrantedAt).IsRequired();
builder.Property(entity => entity.IsActive).IsRequired().HasDefaultValue(true);
builder.Property(entity => entity.Notes).IsRequired(false).HasMaxLength(500);
// رابطه با User
@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddIsActiveToUserClubFeatures : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsActive",
schema: "CMS",
table: "UserClubFeatures",
type: "bit",
nullable: false,
defaultValue: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsActive",
schema: "CMS",
table: "UserClubFeatures");
}
}
}
@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddFlushedFieldsToNetworkWeeklyBalance : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "FlushedPerSide",
schema: "CMS",
table: "NetworkWeeklyBalances",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "TotalFlushed",
schema: "CMS",
table: "NetworkWeeklyBalances",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FlushedPerSide",
schema: "CMS",
table: "NetworkWeeklyBalances");
migrationBuilder.DropColumn(
name: "TotalFlushed",
schema: "CMS",
table: "NetworkWeeklyBalances");
}
}
}
@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddSubordinateBalancesToNetworkWeeklyBalance : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "SubordinateBalances",
schema: "CMS",
table: "NetworkWeeklyBalances",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SubordinateBalances",
schema: "CMS",
table: "NetworkWeeklyBalances");
}
}
}
@@ -204,6 +204,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<DateTime>("GrantedAt")
.HasColumnType("datetime2");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
@@ -1289,6 +1294,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<int>("FlushedPerSide")
.HasColumnType("int");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
@@ -1331,9 +1339,15 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<int>("RightLegTotal")
.HasColumnType("int");
b.Property<int>("SubordinateBalances")
.HasColumnType("int");
b.Property<int>("TotalBalances")
.HasColumnType("int");
b.Property<int>("TotalFlushed")
.HasColumnType("int");
b.Property<long>("UserId")
.HasColumnType("bigint");
@@ -130,7 +130,7 @@ public class DayaLoanApiService : IDayaLoanApiService
"/api/merchant/contracts",
requestBody,
cancellationToken);
var x = await response.Content.ReadAsStringAsync();
// خواندن پاسخ
var apiResponse = await response.Content.ReadFromJsonAsync<DayaContractsResponse>(cancellationToken);
@@ -211,7 +211,7 @@ public class DayaPaymentService : IPaymentGatewayService
{
IsSuccess = false,
Message = "فرمت شماره شبا نامعتبر است",
ProcessedAt = DateTime.UtcNow
ProcessedAt = DateTime.Now
};
}
@@ -222,7 +222,7 @@ public class DayaPaymentService : IPaymentGatewayService
{
IsSuccess = false,
Message = "حداقل مبلغ برداشت 10,000 تومان است",
ProcessedAt = DateTime.UtcNow
ProcessedAt = DateTime.Now
};
}
@@ -252,7 +252,7 @@ public class DayaPaymentService : IPaymentGatewayService
{
IsSuccess = false,
Message = $"خطا در واریز: {response.StatusCode}",
ProcessedAt = DateTime.UtcNow
ProcessedAt = DateTime.Now
};
}
@@ -264,7 +264,7 @@ public class DayaPaymentService : IPaymentGatewayService
{
IsSuccess = false,
Message = "پاسخ نامعتبر از درگاه",
ProcessedAt = DateTime.UtcNow
ProcessedAt = DateTime.Now
};
}
@@ -278,7 +278,7 @@ public class DayaPaymentService : IPaymentGatewayService
BankRefId = result.BankRefId,
TrackingCode = result.TrackingCode,
Message = result.Message,
ProcessedAt = DateTime.UtcNow
ProcessedAt = DateTime.Now
};
}
catch (Exception ex)
@@ -288,7 +288,7 @@ public class DayaPaymentService : IPaymentGatewayService
{
IsSuccess = false,
Message = "خطا در پردازش واریز",
ProcessedAt = DateTime.UtcNow
ProcessedAt = DateTime.Now
};
}
}
@@ -91,7 +91,7 @@ public class MockPaymentGatewayService : IPaymentGatewayService
{
IsSuccess = false,
Message = "فرمت شماره شبا نامعتبر است",
ProcessedAt = DateTime.UtcNow
ProcessedAt = DateTime.Now
};
}
@@ -103,7 +103,7 @@ public class MockPaymentGatewayService : IPaymentGatewayService
{
IsSuccess = false,
Message = "حداقل مبلغ برداشت 10,000 تومان است",
ProcessedAt = DateTime.UtcNow
ProcessedAt = DateTime.Now
};
}
@@ -121,7 +121,7 @@ public class MockPaymentGatewayService : IPaymentGatewayService
BankRefId = bankRefId,
TrackingCode = trackingCode,
Message = $"واریز {request.Amount:N0} تومان به حساب {request.Iban} با موفقیت انجام شد (Mock)",
ProcessedAt = DateTime.UtcNow
ProcessedAt = DateTime.Now
};
}
}