feat: Add monitoring alerts skeleton and enhance worker with notifications
This commit is contained in:
@@ -20,23 +20,37 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
throw new NotFoundException(nameof(User), request.UserId);
|
||||
}
|
||||
|
||||
// دریافت مبلغ عضویت از Configuration
|
||||
var membershipPrice = await _context.SystemConfigurations
|
||||
.Where(x => x.Key == "club_membership_price" && x.IsActive)
|
||||
.Select(x => x.Value)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
long initialContribution = 25_000_000; // Default: 25 million Rials
|
||||
if (!string.IsNullOrEmpty(membershipPrice) && long.TryParse(membershipPrice, out var parsedPrice))
|
||||
{
|
||||
initialContribution = parsedPrice;
|
||||
}
|
||||
|
||||
// بررسی عضویت فعلی
|
||||
var existingMembership = await _context.ClubMemberships
|
||||
.FirstOrDefaultAsync(x => x.UserId == request.UserId, cancellationToken);
|
||||
|
||||
ClubMembership entity;
|
||||
bool isNewMembership = existingMembership == null;
|
||||
var activationDate = request.ActivationDate ?? DateTimeOffset.UtcNow;
|
||||
var activationDate = request.ActivationDate ?? DateTimeOffset.Now; // استفاده از Local Time
|
||||
|
||||
if (isNewMembership)
|
||||
{
|
||||
// ایجاد عضویت جدید
|
||||
// توجه: InitialContribution فقط ثبت میشود، از کیف پول کسر نمیشود!
|
||||
// کاربر قبلاً باید کیف پول خود را شارژ کرده باشد
|
||||
entity = new ClubMembership
|
||||
{
|
||||
UserId = request.UserId,
|
||||
IsActive = true,
|
||||
ActivatedAt = activationDate.DateTime,
|
||||
InitialContribution = 0,
|
||||
InitialContribution = initialContribution, // مبلغ عضویت از Configuration
|
||||
TotalEarned = 0
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس ارسال Alert و Notification
|
||||
/// برای ارسال اعلانهای مختلف از طریق کانالهای مختلف (Email, SMS, Slack, etc.)
|
||||
/// </summary>
|
||||
public interface IAlertService
|
||||
{
|
||||
/// <summary>
|
||||
/// ارسال Alert برای خطاهای Critical
|
||||
/// </summary>
|
||||
Task SendCriticalAlertAsync(string title, string message, Exception? exception = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// ارسال Alert برای Warning
|
||||
/// </summary>
|
||||
Task SendWarningAlertAsync(string title, string message, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// ارسال اعلان موفقیت
|
||||
/// </summary>
|
||||
Task SendSuccessNotificationAsync(string title, string message, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// سرویس ارسال Notification به کاربران
|
||||
/// برای ارسال پیامک، ایمیل و پوش به کاربران سیستم
|
||||
/// </summary>
|
||||
public interface IUserNotificationService
|
||||
{
|
||||
/// <summary>
|
||||
/// ارسال اعلان دریافت کمیسیون به کاربر
|
||||
/// </summary>
|
||||
Task SendCommissionReceivedNotificationAsync(
|
||||
long userId,
|
||||
decimal amount,
|
||||
int weekNumber,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// ارسال اعلان فعالسازی عضویت باشگاه
|
||||
/// </summary>
|
||||
Task SendClubActivationNotificationAsync(
|
||||
long userId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// ارسال اعلان خطا در پرداخت
|
||||
/// </summary>
|
||||
Task SendPayoutErrorNotificationAsync(
|
||||
long userId,
|
||||
string errorMessage,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس محاسبه موقعیت در Binary Tree
|
||||
/// این سرویس مشخص میکند که کاربر جدید باید در کدام Leg (Left/Right) قرار بگیرد
|
||||
/// </summary>
|
||||
public interface INetworkPlacementService
|
||||
{
|
||||
/// <summary>
|
||||
/// محاسبه LegPosition برای کاربر جدید
|
||||
/// </summary>
|
||||
/// <param name="parentId">شناسه Parent در Network</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>
|
||||
/// - Left: اگر Parent فرزند چپ ندارد
|
||||
/// - Right: اگر Parent فرزند راست ندارد
|
||||
/// - null: اگر Parent هر دو Leg را دارد (Binary Tree پر است!)
|
||||
/// </returns>
|
||||
Task<NetworkLeg?> CalculateLegPositionAsync(long parentId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// بررسی اینکه آیا Parent میتواند فرزند جدید بپذیرد
|
||||
/// </summary>
|
||||
/// <param name="parentId"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>true اگر Parent کمتر از 2 فرزند دارد</returns>
|
||||
Task<bool> CanAcceptChildAsync(long parentId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// پیدا کردن اولین Parent در شبکه که میتواند فرزند جدید بپذیرد
|
||||
/// (برای Auto-Placement در Binary Tree)
|
||||
/// </summary>
|
||||
/// <param name="rootParentId">شناسه Parent اصلی که از آن شروع میکنیم</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>شناسه Parent مناسب برای قرار گرفتن کاربر جدید</returns>
|
||||
Task<long?> FindAvailableParentAsync(long rootParentId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,12 +1,23 @@
|
||||
using CMSMicroservice.Domain.Events;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.UserCQ.Commands.CreateNewUser;
|
||||
|
||||
public class CreateNewUserCommandHandler : IRequestHandler<CreateNewUserCommand, CreateNewUserResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly INetworkPlacementService _networkPlacementService;
|
||||
private readonly ILogger<CreateNewUserCommandHandler> _logger;
|
||||
|
||||
public CreateNewUserCommandHandler(IApplicationDbContext context)
|
||||
public CreateNewUserCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
INetworkPlacementService networkPlacementService,
|
||||
ILogger<CreateNewUserCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_networkPlacementService = networkPlacementService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<CreateNewUserResponseDto> Handle(CreateNewUserCommand request,
|
||||
@@ -15,9 +26,71 @@ public class CreateNewUserCommandHandler : IRequestHandler<CreateNewUserCommand,
|
||||
var entity = request.Adapt<User>();
|
||||
entity.ReferralCode = UtilExtensions.Generate(digits: 10, firstDigitNonZero: true);
|
||||
|
||||
// === تنظیم Network Binary Tree ===
|
||||
// اگر ParentId تنظیم شده، باید NetworkParentId و LegPosition هم Set بشن
|
||||
if (request.ParentId.HasValue)
|
||||
{
|
||||
// محاسبه LegPosition برای Binary Tree
|
||||
var legPosition = await _networkPlacementService.CalculateLegPositionAsync(
|
||||
request.ParentId.Value,
|
||||
cancellationToken);
|
||||
|
||||
if (legPosition.HasValue)
|
||||
{
|
||||
// Parent میتواند فرزند جدید بپذیرد
|
||||
entity.NetworkParentId = request.ParentId.Value;
|
||||
entity.LegPosition = legPosition.Value;
|
||||
|
||||
_logger.LogInformation(
|
||||
"User {UserId} placed in Binary Tree: Parent={ParentId}, Leg={Leg}",
|
||||
entity.Id, entity.NetworkParentId, entity.LegPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parent پر است! باید Auto-Placement کنیم یا Error بدیم
|
||||
_logger.LogWarning(
|
||||
"Parent {ParentId} has no available leg! Finding alternative parent...",
|
||||
request.ParentId.Value);
|
||||
|
||||
var availableParent = await _networkPlacementService.FindAvailableParentAsync(
|
||||
request.ParentId.Value,
|
||||
cancellationToken);
|
||||
|
||||
if (availableParent.HasValue)
|
||||
{
|
||||
var newLegPosition = await _networkPlacementService.CalculateLegPositionAsync(
|
||||
availableParent.Value,
|
||||
cancellationToken);
|
||||
|
||||
entity.NetworkParentId = availableParent.Value;
|
||||
entity.LegPosition = newLegPosition!.Value;
|
||||
|
||||
_logger.LogInformation(
|
||||
"User {UserId} auto-placed under alternative Parent={ParentId}, Leg={Leg}",
|
||||
entity.Id, entity.NetworkParentId, entity.LegPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
// هیچ جای خالی در Binary Tree پیدا نشد!
|
||||
_logger.LogError(
|
||||
"No available parent found in network for ParentId={ParentId}",
|
||||
request.ParentId.Value);
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"شبکه Parent با شناسه {request.ParentId.Value} پر است و نمیتواند کاربر جدید بپذیرد.");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// کاربر Root است (بدون Parent)
|
||||
_logger.LogInformation("Creating root user without Parent");
|
||||
}
|
||||
|
||||
await _context.Users.AddAsync(entity, cancellationToken);
|
||||
entity.AddDomainEvent(new CreateNewUserEvent(entity));
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return entity.Adapt<CreateNewUserResponseDto>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.UserCQ.Commands.MigrateNetworkParentId;
|
||||
|
||||
/// <summary>
|
||||
/// Command for manual migration of ParentId → NetworkParentId
|
||||
/// این Command در صورتی که Seeder اجرا نشده یا نیاز به اجرای دستی باشد، استفاده میشود
|
||||
/// </summary>
|
||||
public record MigrateNetworkParentIdCommand : IRequest<MigrateNetworkParentIdResult>;
|
||||
|
||||
public record MigrateNetworkParentIdResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public int MigratedCount { get; init; }
|
||||
public int SkippedCount { get; init; }
|
||||
public List<string> ValidationErrors { get; init; } = new();
|
||||
public string Message { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.UserCQ.Commands.MigrateNetworkParentId;
|
||||
|
||||
public class MigrateNetworkParentIdCommandHandler : IRequestHandler<MigrateNetworkParentIdCommand, MigrateNetworkParentIdResult>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ILogger<MigrateNetworkParentIdCommandHandler> _logger;
|
||||
|
||||
public MigrateNetworkParentIdCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
ILogger<MigrateNetworkParentIdCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<MigrateNetworkParentIdResult> Handle(MigrateNetworkParentIdCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("=== Starting Manual ParentId → NetworkParentId Migration ===");
|
||||
|
||||
var errors = new List<string>();
|
||||
|
||||
// Step 1: Check if already migrated
|
||||
var alreadyMigrated = await _context.Users
|
||||
.Where(u => u.ParentId != null && u.NetworkParentId != null)
|
||||
.AnyAsync(cancellationToken);
|
||||
|
||||
if (alreadyMigrated)
|
||||
{
|
||||
_logger.LogWarning("⚠️ Migration already completed!");
|
||||
return new MigrateNetworkParentIdResult
|
||||
{
|
||||
Success = false,
|
||||
Message = "Migration already completed. All users with ParentId have NetworkParentId."
|
||||
};
|
||||
}
|
||||
|
||||
// Step 2: Find users to migrate
|
||||
var usersToMigrate = await _context.Users
|
||||
.Where(u => u.ParentId != null && u.NetworkParentId == null)
|
||||
.OrderBy(u => u.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (usersToMigrate.Count == 0)
|
||||
{
|
||||
return new MigrateNetworkParentIdResult
|
||||
{
|
||||
Success = true,
|
||||
Message = "No users to migrate. All done!"
|
||||
};
|
||||
}
|
||||
|
||||
// Step 3: Group by ParentId
|
||||
var parentGroups = usersToMigrate.GroupBy(u => u.ParentId);
|
||||
|
||||
int migratedCount = 0;
|
||||
int skippedCount = 0;
|
||||
|
||||
foreach (var group in parentGroups)
|
||||
{
|
||||
var parentId = group.Key;
|
||||
var children = group.OrderBy(u => u.Id).ToList();
|
||||
|
||||
if (children.Count > 2)
|
||||
{
|
||||
var warning = $"Parent {parentId} has {children.Count} children! Taking first 2 only.";
|
||||
_logger.LogWarning(warning);
|
||||
errors.Add(warning);
|
||||
|
||||
skippedCount += (children.Count - 2);
|
||||
children = children.Take(2).ToList();
|
||||
}
|
||||
|
||||
// Assign NetworkParentId and LegPosition
|
||||
for (int i = 0; i < children.Count && i < 2; i++)
|
||||
{
|
||||
var child = children[i];
|
||||
child.NetworkParentId = parentId;
|
||||
child.LegPosition = i == 0 ? NetworkLeg.Left : NetworkLeg.Right;
|
||||
migratedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Save changes
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("✅ Migration Completed! Migrated={Migrated}, Skipped={Skipped}",
|
||||
migratedCount, skippedCount);
|
||||
|
||||
// Step 5: Validate
|
||||
await ValidateAsync(errors, cancellationToken);
|
||||
|
||||
return new MigrateNetworkParentIdResult
|
||||
{
|
||||
Success = true,
|
||||
MigratedCount = migratedCount,
|
||||
SkippedCount = skippedCount,
|
||||
ValidationErrors = errors,
|
||||
Message = $"Migration completed successfully. Migrated: {migratedCount}, Skipped: {skippedCount}"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ValidateAsync(List<string> errors, CancellationToken cancellationToken)
|
||||
{
|
||||
// Check orphaned nodes
|
||||
var orphanedUsers = await _context.Users
|
||||
.Where(u => u.NetworkParentId != null &&
|
||||
!_context.Users.Any(p => p.Id == u.NetworkParentId))
|
||||
.Select(u => u.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (orphanedUsers.Any())
|
||||
{
|
||||
var error = $"Found {orphanedUsers.Count} orphaned users: {string.Join(", ", orphanedUsers)}";
|
||||
_logger.LogError(error);
|
||||
errors.Add(error);
|
||||
}
|
||||
|
||||
// Check binary tree violation
|
||||
var parentsWithTooManyChildren = await _context.Users
|
||||
.Where(u => u.NetworkParentId != null)
|
||||
.GroupBy(u => u.NetworkParentId)
|
||||
.Select(g => new { ParentId = g.Key, Count = g.Count() })
|
||||
.Where(x => x.Count > 2)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (parentsWithTooManyChildren.Any())
|
||||
{
|
||||
var error = $"Binary tree violation! {parentsWithTooManyChildren.Count} parents have >2 children";
|
||||
_logger.LogError(error);
|
||||
errors.Add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user