feat: add IsActive field to UserClubFeatures for admin management
This commit is contained in:
@@ -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)
|
||||
|
||||
+4
-4
@@ -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;
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+3232
File diff suppressed because it is too large
Load Diff
+31
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+3238
File diff suppressed because it is too large
Load Diff
+44
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+3241
File diff suppressed because it is too large
Load Diff
+31
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user