feat: implement Kavenegar SMS service and refactor system configurations to use static constants
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m38s

This commit is contained in:
masoodafar-web
2025-12-26 09:04:27 +03:30
parent 9d2b5ad2d4
commit 02e2f8111f
51 changed files with 4064 additions and 1388 deletions
@@ -1,5 +1,6 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Entities.Club;
using CMSMicroservice.Domain.Entities.Commission;
@@ -126,38 +127,14 @@ public class AcceptClubMembershipContractCommandHandler
};
await _context.UserContracts.AddAsync(userContract, cancellationToken);
// 6. دریافت مقادیر از تنظیمات سیستم
var giftValueConfig = await _context.SystemConfigurations
.FirstOrDefaultAsync(
c => c.Key == "Club.MembershipGiftValue" && c.IsActive,
cancellationToken
);
// 6. دریافت مقادیر از SystemConstants (استاتیک)
long giftValue = SystemConstants.ClubMembershipGiftValue;
long activationFeeValue = SystemConstants.ClubActivationFee;
var activationFeeConfig = await _context.SystemConfigurations
.FirstOrDefaultAsync(
c => c.Key == "Club.ActivationFee" && c.IsActive,
cancellationToken
);
long giftValue = 28_000_000; // مقدار پیش‌فرض
if (giftValueConfig != null && long.TryParse(giftValueConfig.Value, out var configValue))
{
giftValue = configValue;
_logger.LogInformation(
"Using Club.MembershipGiftValue from configuration: {GiftValue}",
giftValue
);
}
long activationFeeValue = 25_200_000; // مقدار پیش‌فرض
if (activationFeeConfig != null && long.TryParse(activationFeeConfig.Value, out var activationFeeConfigValue))
{
activationFeeValue = activationFeeConfigValue;
_logger.LogInformation(
"Using Club.ActivationFee from configuration: {activationFeeValue}",
activationFeeValue
);
}
_logger.LogInformation(
"Using Club.MembershipGiftValue: {GiftValue}, Club.ActivationFee: {ActivationFee}",
giftValue, activationFeeValue
);
// 7. فعال‌سازی باشگاه مشتریان
ClubMembership clubMembership;
@@ -1,6 +1,7 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Entities.Club;
using CMSMicroservice.Domain.Entities.Commission;
@@ -75,7 +76,7 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
throw new NotFoundException("کیف پول کاربر یافت نشد");
}
if (wallet.Balance < 56_000_000)
if (wallet.Balance < SystemConstants.BasePackageAmount)
{
_logger.LogWarning(
"User {UserId} has insufficient balance: {Balance}",
@@ -83,7 +84,7 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
wallet.Balance
);
throw new BadRequestException(
"برای فعالسازی باشگاه مشتریان باید حداقل 56 میلیون تومان موجودی اصلی داشته باشید"
$"برای فعالسازی باشگاه مشتریان باید حداقل {SystemConstants.BasePackageAmount:N0} ریال موجودی اصلی داشته باشید"
);
}
@@ -135,51 +136,14 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
var existingMembership = await _context.ClubMemberships
.FirstOrDefaultAsync(c => c.UserId == user.Id, cancellationToken);
// 6.1. دریافت مبلغ هدیه از تنظیمات
var giftValueConfig = await _context.SystemConfigurations
.FirstOrDefaultAsync(
c => c.Key == "Club.MembershipGiftValue" && c.IsActive,
cancellationToken
);
// 6.1. دریافت مبلغ هدیه و هزینه فعالسازی از SystemConstants
long giftValue = SystemConstants.ClubMembershipGiftValue;
long activationFeeValue = SystemConstants.ClubActivationFee;
var activationFeeConfig = await _context.SystemConfigurations
.FirstOrDefaultAsync(
c => c.Key == "Club.ActivationFee" && c.IsActive,
cancellationToken
);
long giftValue = 28_000_000; // مقدار پیش‌فرض
if (giftValueConfig != null && long.TryParse(giftValueConfig.Value, out var configValue))
{
giftValue = configValue;
_logger.LogInformation(
"Using Club.MembershipGiftValue from configuration: {GiftValue}",
giftValue
);
}
else
{
_logger.LogWarning(
"Club.MembershipGiftValue not found in configuration, using default: {GiftValue}",
giftValue
);
}
long activationFeeValue = 25_200_000; // مقدار پیش‌فرض
if (activationFeeConfig != null && long.TryParse(activationFeeConfig.Value, out var activationFeeConfigValue))
{
activationFeeValue = activationFeeConfigValue;
_logger.LogInformation(
"Using Club.ActivationFee from configuration: {activationFeeValue}",
activationFeeValue
);
}
else
{
_logger.LogWarning(
"Club.ActivationFee not found in configuration, using default: {activationFeeValue}",
activationFeeValue
);
}
_logger.LogInformation(
"Using Club.MembershipGiftValue: {GiftValue}, Club.ActivationFee: {ActivationFee}",
giftValue, activationFeeValue
);
ClubMembership entity;
bool isNewMembership = existingMembership == null;
@@ -1,3 +1,5 @@
using CMSMicroservice.Domain.Common;
namespace CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWeeklyBalancesCommand, int>
@@ -74,21 +76,11 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
var balancesList = new List<NetworkWeeklyBalance>();
var calculatedAt = DateTime.Now;
// خواندن یکباره Configuration ها (بهینه‌سازی - به جای N query)
var configs = await _context.SystemConfigurations
.Where(x => x.IsActive && (
x.Key == "Club.ActivationFee" ||
x.Key == "Commission.WeeklyPoolContributionPercent" ||
x.Key == "Commission.MaxWeeklyBalancesPerLeg" ||
x.Key == "Commission.MaxNetworkLevel"))
.ToDictionaryAsync(x => x.Key, x => x.Value, cancellationToken);
// var activationFee = long.Parse(configs.GetValueOrDefault("Club.ActivationFee", "25000000"));
// var poolPercent = decimal.Parse(configs.GetValueOrDefault("Commission.WeeklyPoolContributionPercent", "20")) / 100m;
// استفاده از SystemConstants (استاتیک - بدون کوئری به دیتابیس)
// سقف تعادل هفتگی برای هر دست (نه کل) - 300 برای چپ + 300 برای راست = حداکثر 600 تعادل
var maxBalancesPerLeg = int.Parse(configs.GetValueOrDefault("Commission.MaxWeeklyBalancesPerLeg", "300"));
var maxBalancesPerLeg = SystemConstants.CommissionMaxWeeklyBalancesPerLeg;
// حداکثر عمق شبکه برای شمارش اعضا (15 لول)
var maxNetworkLevel = int.Parse(configs.GetValueOrDefault("Commission.MaxNetworkLevel", "15"));
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
foreach (var user in usersInNetwork.OrderBy(o=>o.Id))
{
@@ -1,3 +1,5 @@
using CMSMicroservice.Domain.Common;
namespace CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayoutsCommand, int>
@@ -47,12 +49,8 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
await _context.SaveChangesAsync(cancellationToken);
}
// ⭐ خواندن MaxNetworkLevel از Config
var maxNetworkLevelConfig = await _context.SystemConfigurations
.Where(x => x.Key == "Commission.MaxNetworkLevel" && x.IsActive)
.Select(x => x.Value)
.FirstOrDefaultAsync(cancellationToken);
var maxNetworkLevel = int.Parse(maxNetworkLevelConfig ?? "15");
// ⭐ خواندن MaxNetworkLevel از SystemConstants (استاتیک)
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
// دریافت همه تعادل‌های هفتگی (شامل صفرها هم برای محاسبه زیرمجموعه)
var allWeeklyBalances = await _context.NetworkWeeklyBalances
@@ -31,8 +31,6 @@ public interface IApplicationDbContext
DbSet<UserPackagePurchase> UserPackagePurchases { get; }
DbSet<UserWallet> UserWallets { get; }
DbSet<UserWalletChangeLog> UserWalletChangeLogs { get; }
DbSet<SystemConfiguration> SystemConfigurations { get; }
DbSet<SystemConfigurationHistory> SystemConfigurationHistories { get; }
DbSet<ManualPayment> ManualPayments { get; }
DbSet<PublicMessage> PublicMessages { get; }
DbSet<ClubMembership> ClubMemberships { get; }
@@ -0,0 +1,17 @@
namespace CMSMicroservice.Application.Common.Interfaces;
/// <summary>
/// سرویس ارسال SMS با کاوه‌نگار
/// </summary>
public interface IKavenegarService
{
/// <summary>
/// ارسال پیامک ساده
/// </summary>
Task SendAsync(string mobile, string message);
/// <summary>
/// ارسال پیامک با قالب (VerifyLookup)
/// </summary>
Task VerifyLookupAsync(string mobile, string token, string template = "Afrino");
}
@@ -1,17 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.DeactivateConfiguration;
/// <summary>
/// Command برای غیرفعال کردن یک Configuration
/// </summary>
public record DeactivateConfigurationCommand : IRequest<Unit>
{
/// <summary>
/// شناسه Configuration
/// </summary>
public long ConfigurationId { get; init; }
/// <summary>
/// دلیل غیرفعال‌سازی
/// </summary>
public string? Reason { get; init; }
}
@@ -1,51 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.DeactivateConfiguration;
public class DeactivateConfigurationCommandHandler : IRequestHandler<DeactivateConfigurationCommand, Unit>
{
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser;
public DeactivateConfigurationCommandHandler(
IApplicationDbContext context,
ICurrentUserService currentUser)
{
_context = context;
_currentUser = currentUser;
}
public async Task<Unit> Handle(DeactivateConfigurationCommand request, CancellationToken cancellationToken)
{
var entity = await _context.SystemConfigurations
.FirstOrDefaultAsync(x => x.Id == request.ConfigurationId, cancellationToken)
?? throw new NotFoundException(nameof(SystemConfiguration), request.ConfigurationId);
// اگر از قبل غیرفعال است، خطا ندهیم
if (!entity.IsActive)
{
return Unit.Value;
}
var oldValue = entity.Value;
entity.IsActive = false;
_context.SystemConfigurations.Update(entity);
await _context.SaveChangesAsync(cancellationToken);
// ثبت تاریخچه
var history = new SystemConfigurationHistory
{
ConfigurationId = entity.Id,
Scope = entity.Scope,
Key = entity.Key,
OldValue = oldValue,
NewValue = entity.Value,
Reason = request.Reason ?? "Configuration deactivated",
PerformedBy = _currentUser.GetPerformedBy()
};
await _context.SystemConfigurationHistories.AddAsync(history, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
@@ -1,29 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.DeactivateConfiguration;
public class DeactivateConfigurationCommandValidator : AbstractValidator<DeactivateConfigurationCommand>
{
public DeactivateConfigurationCommandValidator()
{
RuleFor(x => x.ConfigurationId)
.GreaterThan(0)
.WithMessage("شناسه Configuration معتبر نیست");
RuleFor(x => x.Reason)
.MaximumLength(500)
.WithMessage("دلیل غیرفعال‌سازی نمی‌تواند بیشتر از 500 کاراکتر باشد")
.When(x => !string.IsNullOrEmpty(x.Reason));
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(
ValidationContext<DeactivateConfigurationCommand>.CreateWithOptions(
(DeactivateConfigurationCommand)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -1,77 +0,0 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.SeedVATConfiguration;
/// <summary>
/// Seed initial VAT configuration
/// نرخ مالیات پیش‌فرض ۹٪
/// </summary>
public class SeedVATConfigurationCommand : IRequest<Unit>
{
}
public class SeedVATConfigurationCommandHandler : IRequestHandler<SeedVATConfigurationCommand, Unit>
{
private readonly IApplicationDbContext _context;
private readonly ILogger<SeedVATConfigurationCommandHandler> _logger;
public SeedVATConfigurationCommandHandler(
IApplicationDbContext context,
ILogger<SeedVATConfigurationCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<Unit> Handle(SeedVATConfigurationCommand request, CancellationToken cancellationToken)
{
var configs = new[]
{
new
{
Scope = ConfigurationScope.VAT,
Key = "Rate",
Value = "0.09",
Description = "نرخ مالیات بر ارزش افزوده (۹٪)"
},
new
{
Scope = ConfigurationScope.VAT,
Key = "IsEnabled",
Value = "true",
Description = "فعال/غیرفعال بودن محاسبه مالیات"
}
};
foreach (var config in configs)
{
var exists = _context.SystemConfigurations
.Any(x => x.Scope == config.Scope && x.Key == config.Key);
if (!exists)
{
_context.SystemConfigurations.Add(new Domain.Entities.Configuration.SystemConfiguration
{
Scope = config.Scope,
Key = config.Key,
Value = config.Value,
Description = config.Description
});
_logger.LogInformation(
"VAT configuration seeded: {Scope}.{Key} = {Value}",
config.Scope,
config.Key,
config.Value
);
}
}
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
@@ -1,32 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.SetConfigurationValue;
/// <summary>
/// Command برای تنظیم یا به‌روزرسانی یک Configuration
/// </summary>
public record SetConfigurationValueCommand : IRequest<long>
{
/// <summary>
/// محدوده تنظیمات (System, Network, Club, Commission)
/// </summary>
public ConfigurationScope Scope { get; init; }
/// <summary>
/// کلید یکتا برای تنظیمات
/// </summary>
public string Key { get; init; }
/// <summary>
/// مقدار تنظیمات (JSON format)
/// </summary>
public string Value { get; init; }
/// <summary>
/// توضیحات تنظیمات
/// </summary>
public string? Description { get; init; }
/// <summary>
/// دلیل تغییر (برای History)
/// </summary>
public string? ChangeReason { get; init; }
}
@@ -1,78 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.SetConfigurationValue;
public class SetConfigurationValueCommandHandler : IRequestHandler<SetConfigurationValueCommand, long>
{
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser;
public SetConfigurationValueCommandHandler(
IApplicationDbContext context,
ICurrentUserService currentUser)
{
_context = context;
_currentUser = currentUser;
}
public async Task<long> Handle(SetConfigurationValueCommand request, CancellationToken cancellationToken)
{
// بررسی وجود Configuration با همین Scope و Key
var existingConfig = await _context.SystemConfigurations
.FirstOrDefaultAsync(x =>
x.Scope == request.Scope &&
x.Key == request.Key,
cancellationToken);
SystemConfiguration entity;
bool isNewRecord = existingConfig == null;
string oldValue = null;
if (isNewRecord)
{
// ایجاد Configuration جدید
entity = new SystemConfiguration
{
Scope = request.Scope,
Key = request.Key,
Value = request.Value,
Description = request.Description,
IsActive = true
};
await _context.SystemConfigurations.AddAsync(entity, cancellationToken);
}
else
{
// به‌روزرسانی Configuration موجود
entity = existingConfig;
oldValue = entity.Value;
entity.Value = request.Value;
if (!string.IsNullOrEmpty(request.Description))
{
entity.Description = request.Description;
}
_context.SystemConfigurations.Update(entity);
}
await _context.SaveChangesAsync(cancellationToken);
// ثبت تاریخچه
var history = new SystemConfigurationHistory
{
ConfigurationId = entity.Id,
Scope = entity.Scope,
Key = entity.Key,
OldValue = oldValue,
NewValue = entity.Value,
Reason = request.ChangeReason ?? (isNewRecord ? "Initial creation" : "Value updated"),
PerformedBy = "System" // TODO: باید از Current User گرفته شود
};
await _context.SystemConfigurationHistories.AddAsync(history, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return entity.Id;
}
}
@@ -1,48 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Commands.SetConfigurationValue;
public class SetConfigurationValueCommandValidator : AbstractValidator<SetConfigurationValueCommand>
{
public SetConfigurationValueCommandValidator()
{
RuleFor(x => x.Scope)
.IsInEnum()
.WithMessage("محدوده تنظیمات معتبر نیست");
RuleFor(x => x.Key)
.NotEmpty()
.WithMessage("کلید تنظیمات الزامی است")
.MaximumLength(100)
.WithMessage("کلید تنظیمات نمی‌تواند بیشتر از 100 کاراکتر باشد")
.Matches(@"^[a-zA-Z0-9_\.]+$")
.WithMessage("کلید تنظیمات فقط می‌تواند شامل حروف انگلیسی، اعداد، نقطه و آندرلاین باشد");
RuleFor(x => x.Value)
.NotEmpty()
.WithMessage("مقدار تنظیمات الزامی است")
.MaximumLength(2000)
.WithMessage("مقدار تنظیمات نمی‌تواند بیشتر از 2000 کاراکتر باشد");
RuleFor(x => x.Description)
.MaximumLength(500)
.WithMessage("توضیحات نمی‌تواند بیشتر از 500 کاراکتر باشد")
.When(x => !string.IsNullOrEmpty(x.Description));
RuleFor(x => x.ChangeReason)
.MaximumLength(500)
.WithMessage("دلیل تغییر نمی‌تواند بیشتر از 500 کاراکتر باشد")
.When(x => !string.IsNullOrEmpty(x.ChangeReason));
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(
ValidationContext<SetConfigurationValueCommand>.CreateWithOptions(
(SetConfigurationValueCommand)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -1,40 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetAllConfigurations;
/// <summary>
/// Query برای دریافت لیست تمام Configuration ها با فیلتر
/// </summary>
public record GetAllConfigurationsQuery : IRequest<GetAllConfigurationsResponseDto>
{
/// <summary>
/// موقعیت صفحه‌بندی
/// </summary>
public PaginationState? PaginationState { get; init; }
/// <summary>
/// مرتب‌سازی بر اساس
/// </summary>
public string? SortBy { get; init; }
/// <summary>
/// فیلتر
/// </summary>
public GetAllConfigurationsFilter? Filter { get; init; }
}
public class GetAllConfigurationsFilter
{
/// <summary>
/// فیلتر بر اساس محدوده
/// </summary>
public ConfigurationScope? Scope { get; set; }
/// <summary>
/// جستجو در کلید
/// </summary>
public string? KeyContains { get; set; }
/// <summary>
/// فقط Configuration های فعال
/// </summary>
public bool? IsActive { get; set; }
}
@@ -1,50 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetAllConfigurations;
public class GetAllConfigurationsQueryHandler : IRequestHandler<GetAllConfigurationsQuery, GetAllConfigurationsResponseDto>
{
private readonly IApplicationDbContext _context;
public GetAllConfigurationsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetAllConfigurationsResponseDto> Handle(GetAllConfigurationsQuery request, CancellationToken cancellationToken)
{
var query = _context.SystemConfigurations
.ApplyOrder(sortBy: request.SortBy)
.AsNoTracking()
.AsQueryable();
if (request.Filter is not null)
{
query = query
.Where(x => request.Filter.Scope == null || x.Scope == request.Filter.Scope)
.Where(x => request.Filter.KeyContains == null || x.Key.Contains(request.Filter.KeyContains))
.Where(x => request.Filter.IsActive == null || x.IsActive == request.Filter.IsActive);
}
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
var models = await query
.PaginatedListAsync(paginationState: request.PaginationState)
.Select(x => new GetAllConfigurationsResponseModel
{
Id = x.Id,
Scope = x.Scope,
Key = x.Key,
Value = x.Value,
Description = x.Description,
IsActive = x.IsActive,
Created = x.Created,
LastModified = x.LastModified
})
.ToListAsync(cancellationToken);
return new GetAllConfigurationsResponseDto
{
MetaData = meta,
Models = models
};
}
}
@@ -1,25 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetAllConfigurations;
public class GetAllConfigurationsQueryValidator : AbstractValidator<GetAllConfigurationsQuery>
{
public GetAllConfigurationsQueryValidator()
{
RuleFor(x => x.Filter.Scope)
.IsInEnum()
.WithMessage("محدوده تنظیمات معتبر نیست")
.When(x => x.Filter?.Scope != null);
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(
ValidationContext<GetAllConfigurationsQuery>.CreateWithOptions(
(GetAllConfigurationsQuery)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -1,19 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetAllConfigurations;
public class GetAllConfigurationsResponseDto
{
public MetaData MetaData { get; set; }
public List<GetAllConfigurationsResponseModel> Models { get; set; }
}
public class GetAllConfigurationsResponseModel
{
public long Id { get; set; }
public ConfigurationScope Scope { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public string? Description { get; set; }
public bool IsActive { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset? LastModified { get; set; }
}
@@ -1,16 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationByKey;
/// <summary>
/// DTO برای نمایش اطلاعات Configuration
/// </summary>
public class ConfigurationDto
{
public long Id { get; set; }
public ConfigurationScope Scope { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public string? Description { get; set; }
public bool IsActive { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset? LastModified { get; set; }
}
@@ -1,17 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationByKey;
/// <summary>
/// Query برای دریافت یک Configuration بر اساس Scope و Key
/// </summary>
public record GetConfigurationByKeyQuery : IRequest<ConfigurationDto?>
{
/// <summary>
/// محدوده تنظیمات
/// </summary>
public ConfigurationScope Scope { get; init; }
/// <summary>
/// کلید تنظیمات
/// </summary>
public string Key { get; init; }
}
@@ -1,34 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationByKey;
public class GetConfigurationByKeyQueryHandler : IRequestHandler<GetConfigurationByKeyQuery, ConfigurationDto?>
{
private readonly IApplicationDbContext _context;
public GetConfigurationByKeyQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<ConfigurationDto?> Handle(GetConfigurationByKeyQuery request, CancellationToken cancellationToken)
{
var config = await _context.SystemConfigurations
.AsNoTracking()
.Where(x => x.Scope == request.Scope && x.Key == request.Key)
.FirstOrDefaultAsync(cancellationToken);
if (config == null)
return null;
return new ConfigurationDto
{
Id = config.Id,
Scope = config.Scope,
Key = config.Key,
Value = config.Value,
Description = config.Description,
IsActive = config.IsActive,
Created = config.Created,
LastModified = config.LastModified
};
}
}
@@ -1,28 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationByKey;
public class GetConfigurationByKeyQueryValidator : AbstractValidator<GetConfigurationByKeyQuery>
{
public GetConfigurationByKeyQueryValidator()
{
RuleFor(x => x.Scope)
.IsInEnum()
.WithMessage("محدوده تنظیمات معتبر نیست");
RuleFor(x => x.Key)
.NotEmpty()
.WithMessage("کلید تنظیمات الزامی است");
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(
ValidationContext<GetConfigurationByKeyQuery>.CreateWithOptions(
(GetConfigurationByKeyQuery)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -1,22 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationHistory;
/// <summary>
/// Query برای دریافت تاریخچه تغییرات یک Configuration
/// </summary>
public record GetConfigurationHistoryQuery : IRequest<GetConfigurationHistoryResponseDto>
{
/// <summary>
/// شناسه Configuration
/// </summary>
public long ConfigurationId { get; init; }
/// <summary>
/// موقعیت صفحه‌بندی
/// </summary>
public PaginationState? PaginationState { get; init; }
/// <summary>
/// مرتب‌سازی بر اساس
/// </summary>
public string? SortBy { get; init; }
}
@@ -1,53 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationHistory;
public class GetConfigurationHistoryQueryHandler : IRequestHandler<GetConfigurationHistoryQuery, GetConfigurationHistoryResponseDto>
{
private readonly IApplicationDbContext _context;
public GetConfigurationHistoryQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetConfigurationHistoryResponseDto> Handle(GetConfigurationHistoryQuery request, CancellationToken cancellationToken)
{
// بررسی وجود Configuration
var configExists = await _context.SystemConfigurations
.AnyAsync(x => x.Id == request.ConfigurationId, cancellationToken);
if (!configExists)
{
throw new NotFoundException(nameof(SystemConfiguration), request.ConfigurationId);
}
var query = _context.SystemConfigurationHistories
.Where(x => x.ConfigurationId == request.ConfigurationId)
.ApplyOrder(sortBy: request.SortBy ?? "Created") // پیش‌فرض: جدیدترین اول
.AsNoTracking()
.AsQueryable();
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
var models = await query
.PaginatedListAsync(paginationState: request.PaginationState)
.Select(x => new GetConfigurationHistoryResponseModel
{
Id = x.Id,
ConfigurationId = x.ConfigurationId,
Scope = x.Scope,
Key = x.Key,
OldValue = x.OldValue,
NewValue = x.NewValue,
ChangeReason = x.Reason,
ChangedBy = x.PerformedBy,
Created = x.Created
})
.ToListAsync(cancellationToken);
return new GetConfigurationHistoryResponseDto
{
MetaData = meta,
Models = models
};
}
}
@@ -1,24 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationHistory;
public class GetConfigurationHistoryQueryValidator : AbstractValidator<GetConfigurationHistoryQuery>
{
public GetConfigurationHistoryQueryValidator()
{
RuleFor(x => x.ConfigurationId)
.GreaterThan(0)
.WithMessage("شناسه Configuration معتبر نیست");
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(
ValidationContext<GetConfigurationHistoryQuery>.CreateWithOptions(
(GetConfigurationHistoryQuery)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -1,20 +0,0 @@
namespace CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationHistory;
public class GetConfigurationHistoryResponseDto
{
public MetaData MetaData { get; set; }
public List<GetConfigurationHistoryResponseModel> Models { get; set; }
}
public class GetConfigurationHistoryResponseModel
{
public long Id { get; set; }
public long ConfigurationId { get; set; }
public ConfigurationScope Scope { get; set; }
public string Key { get; set; }
public string? OldValue { get; set; }
public string NewValue { get; set; }
public string ChangeReason { get; set; }
public string ChangedBy { get; set; }
public DateTimeOffset Created { get; set; }
}
@@ -1,5 +1,6 @@
using CMSMicroservice.Domain.Events;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Application.DayaLoanCQ.Services;
using Microsoft.Extensions.Logging;
@@ -10,11 +11,6 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
private readonly IApplicationDbContext _context;
private readonly IDayaLoanApiService _dayaApiService;
private readonly ILogger<CheckDayaLoanStatusCommandHandler> _logger;
/// <summary>
/// مبلغ وام دایا - 56 میلیون ریال
/// </summary>
private const long DAYA_LOAN_AMOUNT = 56_000_000;
public CheckDayaLoanStatusCommandHandler(
IApplicationDbContext context,
@@ -69,7 +65,7 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
_logger.LogInformation(
"Daya loan processed for User {UserId}, ContractNumber: {ContractNumber}, Amount: {Amount}",
existingContract.UserId, dayaResult.ContractNumber, DAYA_LOAN_AMOUNT);
existingContract.UserId, dayaResult.ContractNumber, SystemConstants.DayaLoanAmount);
}
}
else
@@ -90,7 +86,7 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
_logger.LogInformation(
"Daya loan processed for new contract - User {UserId}, ContractNumber: {ContractNumber}, Amount: {Amount}",
user.Id, dayaResult.ContractNumber, DAYA_LOAN_AMOUNT);
user.Id, dayaResult.ContractNumber, SystemConstants.DayaLoanAmount);
}
var newContract = new DayaLoanContract
@@ -187,13 +183,13 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
var previousBalance = wallet.Balance;
var previousDiscountBalance = wallet.DiscountBalance;
wallet.Balance += DAYA_LOAN_AMOUNT;
wallet.DiscountBalance += DAYA_LOAN_AMOUNT;
wallet.Balance += SystemConstants.DayaLoanAmount;
wallet.DiscountBalance += SystemConstants.DayaLoanAmount;
// 2. ثبت تراکنش - RefId = شماره قرارداد، Status = 0 (Success)، Type = 2 (DepositExternal1)
var transaction = new Transaction
{
Amount = DAYA_LOAN_AMOUNT,
Amount = SystemConstants.DayaLoanAmount,
Description = $"شارژ کیف پول از وام دایا - قرارداد {contractNumber}",
PaymentStatus = PaymentStatus.Success, // 0
PaymentDate = DateTime.Now,
@@ -209,11 +205,11 @@ public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanSt
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = DAYA_LOAN_AMOUNT,
ChangeValue = SystemConstants.DayaLoanAmount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = DAYA_LOAN_AMOUNT,
ChangeDiscountValue = SystemConstants.DayaLoanAmount,
IsIncrease = true,
RefrenceId = transaction.Id
};
@@ -1,4 +1,5 @@
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Common;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
@@ -18,17 +19,17 @@ public record ProcessDayaLoanApprovalCommand : IRequest<ProcessDayaLoanApprovalR
public string ContractNumber { get; init; }
/// <summary>
/// مبلغ کیف پول عادی (56 میلیون)
/// مبلغ کیف پول عادی
/// </summary>
public long WalletAmount { get; init; } = 56_000_000;
public long WalletAmount { get; init; } = SystemConstants.DayaLoanAmount;
/// <summary>
/// مبلغ کیف پول قفل شده (56 میلیون)
/// مبلغ کیف پول قفل شده
/// </summary>
public long LockedWalletAmount { get; init; } = 56_000_000;
public long LockedWalletAmount { get; init; } = SystemConstants.DayaLoanAmount;
/// <summary>
/// مبلغ کیف پول تخفیف (56 میلیون)
/// مبلغ کیف پول تخفیف (دو برابر)
/// </summary>
public long DiscountWalletAmount { get; init; } = 56_000_000;
public long DiscountWalletAmount { get; init; } = SystemConstants.DayaLoanAmount * 2;
}
@@ -76,22 +76,7 @@ public class ProcessDayaLoanApprovalCommandHandler : IRequestHandler<ProcessDaya
};
await _context.UserWalletChangeLogs.AddAsync(mainLog, cancellationToken);
// شارژ کیف پول شبکه/کارمزد (56 میلیون) - نام‌گذاری قدیم: کیف پول قفل شده
var balanceBeforeLocked = wallet.NetworkBalance;
wallet.NetworkBalance += request.LockedWalletAmount;
// لاگ کیف پول شبکه
var networkLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = 0,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = request.LockedWalletAmount,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(networkLog, cancellationToken);
// شارژ کیف پول تخفیف (56 میلیون)
var balanceBeforeDiscount = wallet.DiscountBalance;
@@ -119,9 +104,9 @@ public class ProcessDayaLoanApprovalCommandHandler : IRequestHandler<ProcessDaya
// تنظیم نحوه خرید پکیج به DayaLoan
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan;
// ثبت سفارش پکیج (فعلاً پکیج طلایی)
// ثبت سفارش پکیج (فعلاً پکیج پایه)
var goldenPackage = await _context.Packages
.FirstOrDefaultAsync(p => p.Title.Contains("طلایی") || p.Title.Contains("Golden"), cancellationToken);
.FirstOrDefaultAsync(p => p.Id==4, cancellationToken);
if (goldenPackage != null)
{
@@ -2,6 +2,7 @@ using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Common;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -19,11 +20,6 @@ public class InitiateBasePackagePaymentCommandHandler : IRequestHandler<Initiate
private readonly IApplicationDbContext _context;
private readonly ILogger<InitiateBasePackagePaymentCommandHandler> _logger;
/// <summary>
/// مبلغ پکیج پایه (56 میلیون تومان)
/// </summary>
private const long BasePackageAmount = 56_000_000;
/// <summary>
/// شناسه پکیج پایه در دیتابیس
/// </summary>
@@ -97,7 +93,7 @@ public class InitiateBasePackagePaymentCommandHandler : IRequestHandler<Initiate
Message = "سفارش قبلی در انتظار پرداخت یافت شد.",
OrderId = pendingOrder.Id,
TransactionId = existingTransaction?.Id ?? 0,
Amount = BasePackageAmount
Amount = SystemConstants.BasePackageAmount
};
}
@@ -116,7 +112,7 @@ public class InitiateBasePackagePaymentCommandHandler : IRequestHandler<Initiate
// 5. ایجاد Transaction با وضعیت Pending
var transaction = new Transaction
{
Amount = BasePackageAmount,
Amount = SystemConstants.BasePackageAmount,
Description = $"خرید پکیج پایه ۵۶ میلیونی - کاربر #{user.Id}",
PaymentStatus = PaymentStatus.Pending,
Type = TransactionType.DepositIpg
@@ -135,7 +131,7 @@ public class InitiateBasePackagePaymentCommandHandler : IRequestHandler<Initiate
{
UserId = user.Id,
PackageId = BasePackageId,
Amount = BasePackageAmount,
Amount = SystemConstants.BasePackageAmount,
PaymentStatus = PaymentStatus.Pending,
DeliveryStatus = DeliveryStatus.None,
UserAddressId = defaultAddress.Id,
@@ -158,7 +154,7 @@ public class InitiateBasePackagePaymentCommandHandler : IRequestHandler<Initiate
Message = "تراکنش و سفارش با موفقیت ثبت شد.",
OrderId = order.Id,
TransactionId = transaction.Id,
Amount = BasePackageAmount
Amount = SystemConstants.BasePackageAmount
};
}
catch (Exception ex)
@@ -2,6 +2,7 @@ using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Common;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -24,11 +25,6 @@ public class VerifyBasePackagePaymentCommandHandler : IRequestHandler<VerifyBase
{
private readonly IApplicationDbContext _context;
private readonly ILogger<VerifyBasePackagePaymentCommandHandler> _logger;
/// <summary>
/// مبلغ پکیج پایه (56 میلیون تومان)
/// </summary>
private const long BasePackageAmount = 56_000_000;
public VerifyBasePackagePaymentCommandHandler(
IApplicationDbContext context,
@@ -136,8 +132,8 @@ public class VerifyBasePackagePaymentCommandHandler : IRequestHandler<VerifyBase
var oldBalance = userWallet.Balance;
var oldDiscountBalance = userWallet.DiscountBalance;
userWallet.Balance += BasePackageAmount;
userWallet.DiscountBalance += BasePackageAmount;
userWallet.Balance += SystemConstants.BasePackageAmount;
userWallet.DiscountBalance += SystemConstants.BasePackageAmount;
_logger.LogInformation(
"Charging wallet for user {UserId}. Balance: {OldBalance} -> {NewBalance}, DiscountBalance: {OldDiscount} -> {NewDiscount}",
@@ -157,11 +153,11 @@ public class VerifyBasePackagePaymentCommandHandler : IRequestHandler<VerifyBase
{
WalletId = userWallet.Id,
CurrentBalance = userWallet.Balance,
ChangeValue = BasePackageAmount,
ChangeValue = SystemConstants.BasePackageAmount,
CurrentNetworkBalance = userWallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = userWallet.DiscountBalance,
ChangeDiscountValue = BasePackageAmount,
ChangeDiscountValue = SystemConstants.BasePackageAmount,
IsIncrease = true,
RefrenceId = transaction.Id
};
@@ -1,3 +1,4 @@
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Domain.Events;
using CMSMicroservice.Domain.Entities.Order;
@@ -63,16 +64,11 @@ public class
}
long finalAmount = 0;
var vatIsEnable =
_context.SystemConfigurations
.FirstOrDefault(f => f.Scope == ConfigurationScope.VAT && f.Key == "IsEnabled")?.Value ??
"0";
if (vatIsEnable=="1")
// استفاده از SystemConstants برای VAT
if (SystemConstants.ShopVATEnabled)
{
_vatRate = float.Parse(
_context.SystemConfigurations
.FirstOrDefault(f => f.Scope == ConfigurationScope.VAT && f.Key == "Shop.VAT")?.Value ??
throw new InvalidOperationException());
_vatRate = (float)SystemConstants.ShopVAT;
finalAmount = AddVAT(user.UserCarts.Sum(s => s.Count * s.Product.Price));
_logger.LogInformation(
"Calculating final amount with VAT. Base Amount: {BaseAmount}, VAT Rate: {VATRate}, Final Amount: {FinalAmount}",
@@ -147,7 +143,7 @@ public class
{
ProductId = s.ProductId,
Count = s.Count,
UnitPrice =vatIsEnable=="1" ?AddVAT(s.Product.Price): s.Product.Price,
UnitPrice = SystemConstants.ShopVATEnabled ? AddVAT(s.Product.Price) : s.Product.Price,
OrderId = newOrder.Id
});
await _context.FactorDetails.AddRangeAsync(factorDetailsList, cancellationToken);
@@ -183,25 +179,15 @@ public class
{
try
{
// بررسی فعال بودن VAT
var vatEnabledConfig = await _context.SystemConfigurations
.FirstOrDefaultAsync(x => x.Scope == ConfigurationScope.VAT && x.Key == "IsEnabled", cancellationToken);
if (vatEnabledConfig == null || !bool.TryParse(vatEnabledConfig.Value, out var isEnabled) || !isEnabled)
// بررسی فعال بودن VAT از SystemConstants
if (!SystemConstants.ShopVATEnabled)
{
_logger.LogInformation("VAT is disabled. Skipping VAT calculation for order {OrderId}", orderId);
return false;
}
// دریافت نرخ VAT
var vatRateConfig = await _context.SystemConfigurations
.FirstOrDefaultAsync(x => x.Scope == ConfigurationScope.VAT && x.Key == "Shop.VAT", cancellationToken);
if (vatRateConfig == null || !decimal.TryParse(vatRateConfig.Value, out var vatRate))
{
_logger.LogWarning("VAT Rate configuration not found or invalid. Using default 0.09");
vatRate = 0.09m;
}
// دریافت نرخ VAT از SystemConstants
var vatRate = SystemConstants.ShopVAT;
// محاسبه مالیات
var vatAmount = (long)(orderAmount * vatRate);
@@ -0,0 +1,176 @@
namespace CMSMicroservice.Domain.Common;
/// <summary>
/// تنظیمات ثابت سیستم - Static و در مموری
/// این مقادیر هرگز تغییر نمی‌کنند و نیازی به جدول ندارند
/// </summary>
public static class SystemConstants
{
#region Network Settings
/// <summary>
/// اجازه حذف والدین که فرزند دارند
/// </summary>
public const bool NetworkAllowOrphanNodes = false;
/// <summary>
/// حداکثر تعداد فرزند مستقیم در هر پا
/// </summary>
public const int NetworkMaxChildrenPerLeg = 1;
#endregion
#region Club Settings
/// <summary>
/// مبلغ هدیه حق عضویت باشگاه (ریال) - این مبلغ از کیف پول کم نمی‌شود
/// </summary>
public const long ClubMembershipGiftValue = 25_200_000;
/// <summary>
/// هزینه فعال‌سازی عضویت باشگاه (ریال)
/// </summary>
public const long ClubActivationFee = 25_200_000;
#endregion
#region Package Settings
/// <summary>
/// مبلغ پکیج طلایی / پایه (ریال) - 56 میلیون تومان
/// شامل: هدیه باشگاه + هزینه فعال‌سازی + مزایای دیگر
/// </summary>
public const long BasePackageAmount = 56_000_000;
/// <summary>
/// مبلغ وام دایا (ریال) - همان مبلغ پکیج طلایی
/// </summary>
public const long DayaLoanAmount = 56_000_000;
#endregion
#region Commission Settings
/// <summary>
/// امکان برداشت نقدی فعال باشد
/// </summary>
public const bool CommissionCashWithdrawalEnabled = true;
/// <summary>
/// حداقل مبلغ برداشت (ریال)
/// </summary>
public const long CommissionMinWithdrawalAmount = 1_000_000;
/// <summary>
/// سقف تعادل هفتگی برای هر دست (چپ یا راست) - حداکثر کل = 600
/// </summary>
public const int CommissionMaxWeeklyBalancesPerLeg = 300;
/// <summary>
/// حداکثر عمق شبکه برای محاسبه کمیسیون (تعداد لول زیرمجموعه)
/// </summary>
public const int CommissionMaxNetworkLevel = 15;
/// <summary>
/// روش محاسبه (ORM یا SP)
/// </summary>
public const string CommissionCalculationStrategy = "SP";
#endregion
#region System Settings
/// <summary>
/// حالت تعمیر و نگهداری سیستم
/// </summary>
public const bool SystemMaintenanceMode = false;
/// <summary>
/// فعال‌سازی لاگ تغییرات
/// </summary>
public const bool SystemEnableAuditLog = true;
#endregion
#region Shop Settings
/// <summary>
/// مالیات بر ارزش افزوده (10%)
/// </summary>
public const decimal ShopVAT = 0.1m;
/// <summary>
/// مالیات فعال است؟
/// </summary>
public const bool ShopVATEnabled = true;
#endregion
#region Helper Methods
/// <summary>
/// دریافت مقدار به صورت دیکشنری برای نمایش در Admin Panel
/// </summary>
public static Dictionary<string, object> GetAllAsDict()
{
return new Dictionary<string, object>
{
// Network
["Network.AllowOrphanNodes"] = NetworkAllowOrphanNodes,
["Network.MaxChildrenPerLeg"] = NetworkMaxChildrenPerLeg,
// Club
["Club.MembershipGiftValue"] = ClubMembershipGiftValue,
["Club.ActivationFee"] = ClubActivationFee,
// Commission
["Commission.CashWithdrawalEnabled"] = CommissionCashWithdrawalEnabled,
["Commission.MinWithdrawalAmount"] = CommissionMinWithdrawalAmount,
["Commission.MaxWeeklyBalancesPerLeg"] = CommissionMaxWeeklyBalancesPerLeg,
["Commission.MaxNetworkLevel"] = CommissionMaxNetworkLevel,
["Commission.CalculationStrategy"] = CommissionCalculationStrategy,
// System
["System.MaintenanceMode"] = SystemMaintenanceMode,
["System.EnableAuditLog"] = SystemEnableAuditLog,
// Shop
["Shop.VAT"] = ShopVAT,
["Shop.VATEnabled"] = ShopVATEnabled
};
}
/// <summary>
/// دریافت لیست تنظیمات با توضیحات
/// </summary>
public static List<(string Key, object Value, string Description)> GetAllWithDescriptions()
{
return new List<(string Key, object Value, string Description)>
{
// Network
("Network.AllowOrphanNodes", NetworkAllowOrphanNodes, "اجازه حذف والدین که فرزند دارند"),
("Network.MaxChildrenPerLeg", NetworkMaxChildrenPerLeg, "حداکثر تعداد فرزند مستقیم در هر پا"),
// Club
("Club.MembershipGiftValue", ClubMembershipGiftValue, "مبلغ هدیه حق عضویت باشگاه (ریال)"),
("Club.ActivationFee", ClubActivationFee, "هزینه فعال‌سازی عضویت باشگاه (ریال)"),
// Commission
("Commission.CashWithdrawalEnabled", CommissionCashWithdrawalEnabled, "امکان برداشت نقدی فعال باشد"),
("Commission.MinWithdrawalAmount", CommissionMinWithdrawalAmount, "حداقل مبلغ برداشت (ریال)"),
("Commission.MaxWeeklyBalancesPerLeg", CommissionMaxWeeklyBalancesPerLeg, "سقف تعادل هفتگی برای هر دست"),
("Commission.MaxNetworkLevel", CommissionMaxNetworkLevel, "حداکثر عمق شبکه برای محاسبه کمیسیون"),
("Commission.CalculationStrategy", CommissionCalculationStrategy, "روش محاسبه (ORM/SP)"),
// System
("System.MaintenanceMode", SystemMaintenanceMode, "حالت تعمیر و نگهداری سیستم"),
("System.EnableAuditLog", SystemEnableAuditLog, "فعال‌سازی لاگ تغییرات"),
// Shop
("Shop.VAT", ShopVAT, "مالیات بر ارزش افزوده"),
("Shop.VATEnabled", ShopVATEnabled, "مالیات فعال است؟")
};
}
#endregion
}
@@ -1,42 +0,0 @@
namespace CMSMicroservice.Domain.Entities.Configuration;
/// <summary>
/// تنظیمات پویای سیستم - قابل تغییر بدون Deployment
/// </summary>
public class SystemConfiguration : BaseAuditableEntity
{
/// <summary>
/// محدوده تنظیمات (System, Network, Club, Commission)
/// </summary>
public ConfigurationScope Scope { get; set; }
/// <summary>
/// کلید تنظیم (مثلاً "MaxWeeklyBalancesPerUser")
/// </summary>
public string Key { get; set; }
/// <summary>
/// مقدار به‌صورت رشته (تفسیر در Application Layer)
/// </summary>
public string Value { get; set; }
/// <summary>
/// نوع داده برای Validation و UI (Int/Decimal/Bool/String/Json)
/// </summary>
public string? DataType { get; set; }
/// <summary>
/// توضیحات برای ادمین
/// </summary>
public string? Description { get; set; }
/// <summary>
/// فعال یا غیرفعال
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// SystemConfigurationHistory Collection Navigation Reference
/// </summary>
public virtual ICollection<SystemConfigurationHistory>? SystemConfigurationHistories { get; set; }
}
@@ -1,47 +0,0 @@
namespace CMSMicroservice.Domain.Entities.History;
/// <summary>
/// تاریخچه تغییرات تنظیمات سیستم (برای Audit)
/// </summary>
public class SystemConfigurationHistory : BaseAuditableEntity
{
/// <summary>
/// شناسه تنظیم
/// </summary>
public long ConfigurationId { get; set; }
/// <summary>
/// SystemConfiguration Navigation Property
/// </summary>
public virtual SystemConfiguration? Configuration { get; set; }
/// <summary>
/// محدوده تنظیمات
/// </summary>
public ConfigurationScope Scope { get; set; }
/// <summary>
/// کلید تنظیم
/// </summary>
public string Key { get; set; }
/// <summary>
/// مقدار قبل از تغییر
/// </summary>
public string OldValue { get; set; }
/// <summary>
/// مقدار بعد از تغییر
/// </summary>
public string NewValue { get; set; }
/// <summary>
/// دلیل تغییر (اختیاری)
/// </summary>
public string? Reason { get; set; }
/// <summary>
/// چه کسی انجام داده (UserId یا "System")
/// </summary>
public string? PerformedBy { get; set; }
}
@@ -1,32 +0,0 @@
namespace CMSMicroservice.Domain.Enums;
/// <summary>
/// محدوده تنظیمات سیستم (Scope)
/// </summary>
public enum ConfigurationScope
{
/// <summary>
/// تنظیمات کلی سیستم
/// </summary>
System = 0,
/// <summary>
/// تنظیمات شبکه باینری
/// </summary>
Network = 1,
/// <summary>
/// تنظیمات باشگاه مشتریان
/// </summary>
Club = 2,
/// <summary>
/// تنظیمات کمیسیون
/// </summary>
Commission = 3,
/// <summary>
/// تنظیمات مالیات بر ارزش افزوده
/// </summary>
VAT = 4
}
@@ -34,6 +34,7 @@ public static class ConfigureServices
services.AddScoped<INetworkPlacementService, NetworkPlacementService>();
services.AddScoped<IAlertService, AlertService>();
services.AddScoped<IUserNotificationService, UserNotificationService>();
services.AddScoped<IKavenegarService, KavenegarService>();
// Daya Loan API Service - قابل تغییر بین Mock و Real
var useMockDayaApi = configuration.GetValue<bool>("DayaApi:UseMock", false);
@@ -84,9 +84,7 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
// ============= Network Club System DbSets =============
// Configuration
public DbSet<SystemConfiguration> SystemConfigurations => Set<SystemConfiguration>();
public DbSet<SystemConfigurationHistory> SystemConfigurationHistories => Set<SystemConfigurationHistory>();
// App Version
public DbSet<AppVersion> AppVersions => Set<AppVersion>();
// Club Management
@@ -1,12 +1,9 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using CMSMicroservice.Domain.Entities.Configuration;
using CMSMicroservice.Domain.Enums;
using System.Collections.Generic;
namespace CMSMicroservice.Infrastructure.Persistence;
public class ApplicationDbContextInitialiser
public class ApplicationDbContextInitialiser
{
private readonly ApplicationDbContext _context;
private readonly ILogger<ApplicationDbContextInitialiser> _logger;
@@ -32,6 +29,7 @@ public class ApplicationDbContextInitialiser
throw;
}
}
public async Task SeedAsync()
{
try
@@ -44,113 +42,12 @@ public class ApplicationDbContextInitialiser
throw;
}
}
public async Task TrySeedAsync()
public Task TrySeedAsync()
{
// Seed / upsert default System Configurations for Network-Club-Commission System
var desiredConfigurations = new List<SystemConfiguration>
{
// Network Configuration
new SystemConfiguration
{
Key = "Network.MaxNetworkDepth",
Value = "15",
Description = "حداکثر عمق شبکه باینری",
Scope = ConfigurationScope.Network,
IsActive = true
},
new SystemConfiguration
{
Key = "Network.MaxChildrenPerLeg",
Value = "1",
Description = "حداکثر تعداد فرزند مستقیم در هر پا",
Scope = ConfigurationScope.Network,
IsActive = true
},
// Commission Configuration
new SystemConfiguration
{
Key = "Commission.MaxWeeklyBalancesPerLeg",
Value = "300",
Description = "سقف تعادل هفتگی برای هر دست (چپ یا راست) - حداکثر کل = 600",
Scope = ConfigurationScope.Commission,
IsActive = true
},
new SystemConfiguration
{
Key = "Commission.MaxNetworkLevel",
Value = "15",
Description = "حداکثر عمق شبکه برای محاسبه کمیسیون (تعداد لول زیرمجموعه)",
Scope = ConfigurationScope.Commission,
IsActive = true
},
new SystemConfiguration
{
Key = "Commission.MinWithdrawalAmount",
Value = "1000000",
Description = "حداقل مبلغ برداشت (ریال)",
Scope = ConfigurationScope.Commission,
IsActive = true
},
new SystemConfiguration
{
Key = "Commission.DefaultInitialContribution",
Value = "25000000",
Description = "مبلغ پیش‌فرض مشارکت/هزینه فعال‌سازی",
Scope = ConfigurationScope.Commission,
IsActive = true
},
new SystemConfiguration
{
Key = "Commission.WeeklyPoolContributionPercent",
Value = "20",
Description = "درصد مشارکت در استخر هفتگی از کل فعال‌سازی‌های جدید شبکه (20%)",
Scope = ConfigurationScope.Commission,
IsActive = true
},
// Club Configuration
new SystemConfiguration
{
Key = "Club.ActivationFee",
Value = "25000000",
Description = "هزینه فعال‌سازی عضویت باشگاه (ریال)",
Scope = ConfigurationScope.Club,
IsActive = true
},
new SystemConfiguration
{
Key = "Club.MembershipGiftValue",
Value = "25200000",
Description = "مبلغ هدیه حق عضویت باشگاه (ریال) - این مبلغ از کیف پول کم نمی‌شود",
Scope = ConfigurationScope.Club,
IsActive = true
},
// System Configuration
new SystemConfiguration
{
Key = "System.EnableAuditLog",
Value = "true",
Description = "فعال‌سازی لاگ تغییرات",
Scope = ConfigurationScope.System,
IsActive = true
}
};
var existingKeys = _context.SystemConfigurations
.Select(c => c.Key)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var newConfigs = desiredConfigurations
.Where(c => !existingKeys.Contains(c.Key))
.ToList();
if (newConfigs.Any())
{
await _context.SystemConfigurations.AddRangeAsync(newConfigs);
await _context.SaveChangesAsync();
_logger.LogInformation("Seeded {Count} default system configurations", newConfigs.Count);
}
// SystemConfigurations دیگه در دیتابیس نیست
// مقادیر کانفیگ حالا در SystemConstants.cs به صورت const تعریف شدن
_logger.LogInformation("Database seeding completed. System configurations are now defined as compile-time constants in SystemConstants.cs");
return Task.CompletedTask;
}
}
@@ -1,35 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
/// <summary>
/// تنظیمات پویای سیستم
/// </summary>
public class SystemConfigurationConfiguration : IEntityTypeConfiguration<SystemConfiguration>
{
public void Configure(EntityTypeBuilder<SystemConfiguration> builder)
{
builder.HasQueryFilter(p => !p.IsDeleted);
builder.Ignore(entity => entity.DomainEvents);
builder.HasKey(entity => entity.Id);
builder.Property(entity => entity.Id).UseIdentityColumn();
builder.Property(entity => entity.Scope).IsRequired();
builder.Property(entity => entity.Key).IsRequired().HasMaxLength(200);
builder.Property(entity => entity.Value).IsRequired().HasMaxLength(1000);
builder.Property(entity => entity.DataType).IsRequired(false).HasMaxLength(50);
builder.Property(entity => entity.Description).IsRequired(false).HasMaxLength(500);
builder.Property(entity => entity.IsActive).IsRequired();
// Composite Index برای جستجوی سریع
builder.HasIndex(e => new { e.Scope, e.Key })
.IsUnique()
.HasDatabaseName("IX_SystemConfiguration_Scope_Key");
// Index برای IsActive
builder.HasIndex(e => e.IsActive)
.HasDatabaseName("IX_SystemConfiguration_IsActive");
}
}
@@ -1,41 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
/// <summary>
/// تاریخچه تغییرات تنظیمات سیستم
/// </summary>
public class SystemConfigurationHistoryConfiguration : IEntityTypeConfiguration<SystemConfigurationHistory>
{
public void Configure(EntityTypeBuilder<SystemConfigurationHistory> builder)
{
builder.HasQueryFilter(p => !p.IsDeleted);
builder.Ignore(entity => entity.DomainEvents);
builder.HasKey(entity => entity.Id);
builder.Property(entity => entity.Id).UseIdentityColumn();
builder.Property(entity => entity.ConfigurationId).IsRequired();
builder.Property(entity => entity.Scope).IsRequired();
builder.Property(entity => entity.Key).IsRequired().HasMaxLength(200);
builder.Property(entity => entity.OldValue).IsRequired().HasMaxLength(1000);
builder.Property(entity => entity.NewValue).IsRequired().HasMaxLength(1000);
builder.Property(entity => entity.Reason).IsRequired(false).HasMaxLength(500);
builder.Property(entity => entity.PerformedBy).IsRequired(false).HasMaxLength(100);
// رابطه با SystemConfiguration
builder.HasOne(entity => entity.Configuration)
.WithMany(sc => sc.SystemConfigurationHistories)
.HasForeignKey(entity => entity.ConfigurationId)
.OnDelete(DeleteBehavior.Restrict);
// Index برای ConfigurationId و Created
builder.HasIndex(e => new { e.ConfigurationId, e.Created })
.HasDatabaseName("IX_SystemConfigurationHistory_ConfigId_Created");
// Index برای Scope و Key
builder.HasIndex(e => new { e.Scope, e.Key })
.HasDatabaseName("IX_SystemConfigurationHistory_Scope_Key");
}
}
@@ -0,0 +1,108 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class RemoveSystemConfigurationsTables : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SystemConfigurationHistories",
schema: "CMS");
migrationBuilder.DropTable(
name: "SystemConfigurations",
schema: "CMS");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SystemConfigurations",
schema: "CMS",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
DataType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
Key = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
Scope = table.Column<int>(type: "int", nullable: false),
Value = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SystemConfigurations", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SystemConfigurationHistories",
schema: "CMS",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ConfigurationId = table.Column<long>(type: "bigint", nullable: false),
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
Key = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
NewValue = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
OldValue = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
PerformedBy = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Reason = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
Scope = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SystemConfigurationHistories", x => x.Id);
table.ForeignKey(
name: "FK_SystemConfigurationHistories_SystemConfigurations_ConfigurationId",
column: x => x.ConfigurationId,
principalSchema: "CMS",
principalTable: "SystemConfigurations",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_SystemConfigurationHistory_ConfigId_Created",
schema: "CMS",
table: "SystemConfigurationHistories",
columns: new[] { "ConfigurationId", "Created" });
migrationBuilder.CreateIndex(
name: "IX_SystemConfigurationHistory_Scope_Key",
schema: "CMS",
table: "SystemConfigurationHistories",
columns: new[] { "Scope", "Key" });
migrationBuilder.CreateIndex(
name: "IX_SystemConfiguration_IsActive",
schema: "CMS",
table: "SystemConfigurations",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_SystemConfiguration_Scope_Key",
schema: "CMS",
table: "SystemConfigurations",
columns: new[] { "Scope", "Key" },
unique: true);
}
}
}
@@ -503,65 +503,6 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.ToTable("AppVersions", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Configuration.SystemConfiguration", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("DataType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<DateTime?>("LastModified")
.HasColumnType("datetime2");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<int>("Scope")
.HasColumnType("int");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.HasKey("Id");
b.HasIndex("IsActive")
.HasDatabaseName("IX_SystemConfiguration_IsActive");
b.HasIndex("Scope", "Key")
.IsUnique()
.HasDatabaseName("IX_SystemConfiguration_Scope_Key");
b.ToTable("SystemConfigurations", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Contract", b =>
{
b.Property<long>("Id")
@@ -1504,69 +1445,6 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.ToTable("NetworkMembershipHistories", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.History.SystemConfigurationHistory", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<long>("ConfigurationId")
.HasColumnType("bigint");
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<DateTime?>("LastModified")
.HasColumnType("datetime2");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("NewValue")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("OldValue")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("PerformedBy")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Reason")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("Scope")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ConfigurationId", "Created")
.HasDatabaseName("IX_SystemConfigurationHistory_ConfigId_Created");
b.HasIndex("Scope", "Key")
.HasDatabaseName("IX_SystemConfigurationHistory_Scope_Key");
b.ToTable("SystemConfigurationHistories", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Network.NetworkWeeklyBalance", b =>
{
b.Property<long>("Id")
@@ -3238,17 +3116,6 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Navigation("WeekDefinition");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.History.SystemConfigurationHistory", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.Configuration.SystemConfiguration", "Configuration")
.WithMany("SystemConfigurationHistories")
.HasForeignKey("ConfigurationId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Configuration");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Network.NetworkWeeklyBalance", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.User", "User")
@@ -3553,11 +3420,6 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Navigation("UserCommissionPayouts");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Configuration.SystemConfiguration", b =>
{
b.Navigation("SystemConfigurationHistories");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Contract", b =>
{
b.Navigation("UserContracts");
@@ -65,19 +65,11 @@ BEGIN
AND IsActive = 1;
-- =============================================
-- 4. خواندن Configuration ها
-- 4. مقادیر ثابت (Hardcoded - از SystemConstants)
-- =============================================
SELECT @MaxBalancesPerLeg = CAST(Value AS INT)
FROM CMS.SystemConfigurations
WHERE [Key] = 'Commission.MaxWeeklyBalancesPerLeg' AND IsActive = 1;
SELECT @MaxNetworkLevel = CAST(Value AS INT)
FROM CMS.SystemConfigurations
WHERE [Key] = 'Commission.MaxNetworkLevel' AND IsActive = 1;
-- مقادیر پیش‌فرض
SET @MaxBalancesPerLeg = ISNULL(@MaxBalancesPerLeg, 300);
SET @MaxNetworkLevel = ISNULL(@MaxNetworkLevel, 15);
-- این مقادیر ثابت هستند و تغییر نمی‌کنند
SET @MaxBalancesPerLeg = 300; -- سقف تعادل هر پا
SET @MaxNetworkLevel = 15; -- حداکثر عمق شبکه
-- =============================================
-- 5. ایجاد جدول موقت برای نتایج
@@ -1,4 +1,5 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@@ -13,13 +14,6 @@ public class CommissionCalculationStrategyFactory : ICommissionCalculationStrate
private readonly IWeekDefinitionRepository _weekRepository;
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// کلید Config برای انتخاب استراتژی
/// مقدار: "ORM" یا "SP"
/// پیش‌فرض: "ORM"
/// </summary>
private const string ConfigKey = "Commission.CalculationStrategy";
public CommissionCalculationStrategyFactory(
IApplicationDbContext context,
IWeekDefinitionRepository weekRepository,
@@ -31,13 +25,10 @@ public class CommissionCalculationStrategyFactory : ICommissionCalculationStrate
}
/// <inheritdoc />
public async Task<ICommissionCalculationStrategy> CreateStrategyAsync(CancellationToken cancellationToken = default)
public Task<ICommissionCalculationStrategy> CreateStrategyAsync(CancellationToken cancellationToken = default)
{
// خواندن Config از دیتابیس
var config = await _context.SystemConfigurations
.FirstOrDefaultAsync(x => x.Key == ConfigKey && x.IsActive, cancellationToken);
var strategyValue = config?.Value?.ToUpperInvariant() ?? "ORM";
// خواندن Config از SystemConstants (استاتیک)
var strategyValue = SystemConstants.CommissionCalculationStrategy.ToUpperInvariant();
var strategyType = strategyValue switch
{
@@ -45,7 +36,7 @@ public class CommissionCalculationStrategyFactory : ICommissionCalculationStrate
_ => CommissionCalculationStrategyType.Orm
};
return CreateStrategy(strategyType);
return Task.FromResult(CreateStrategy(strategyType));
}
/// <inheritdoc />
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Domain.Entities.Club;
using CMSMicroservice.Domain.Entities.Commission;
using CMSMicroservice.Domain.Entities.Network;
@@ -87,15 +88,9 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy
var balancesList = new List<NetworkWeeklyBalance>();
var calculatedAt = DateTime.Now;
// خواندن یکباره Configuration ها
var configs = await _context.SystemConfigurations
.Where(x => x.IsActive && (
x.Key == "Commission.MaxWeeklyBalancesPerLeg" ||
x.Key == "Commission.MaxNetworkLevel"))
.ToDictionaryAsync(x => x.Key, x => x.Value, cancellationToken);
var maxBalancesPerLeg = int.Parse(configs.GetValueOrDefault("Commission.MaxWeeklyBalancesPerLeg", "300"));
var maxNetworkLevel = int.Parse(configs.GetValueOrDefault("Commission.MaxNetworkLevel", "15"));
// استفاده از SystemConstants به جای دیتابیس
var maxBalancesPerLeg = SystemConstants.CommissionMaxWeeklyBalancesPerLeg;
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
foreach (var user in usersInNetwork.OrderBy(o => o.Id))
{
@@ -0,0 +1,109 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Infrastructure.Configuration;
using Kavenegar;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace CMSMicroservice.Infrastructure.Services;
/// <summary>
/// پیاده‌سازی سرویس ارسال SMS با کاوه‌نگار
/// </summary>
public class KavenegarService : IKavenegarService
{
private readonly KavenegarApi? _kavenegarApi;
private readonly SmsSettings _smsSettings;
private readonly ILogger<KavenegarService> _logger;
public KavenegarService(
IOptions<SmsSettings> smsSettings,
ILogger<KavenegarService> logger)
{
_smsSettings = smsSettings.Value;
_logger = logger;
// Initialize Kavenegar API
if (_smsSettings.Enabled && !string.IsNullOrEmpty(_smsSettings.KavenegarApiKey))
{
try
{
_kavenegarApi = new KavenegarApi(_smsSettings.KavenegarApiKey);
}
catch (Exception ex)
{
_logger.LogError(ex, "❌ Failed to initialize Kavenegar API");
}
}
}
/// <inheritdoc />
public async Task SendAsync(string mobile, string message)
{
if (!_smsSettings.Enabled)
{
_logger.LogInformation("SMS is disabled. Skipping send to {Mobile}", mobile);
return;
}
if (_kavenegarApi == null)
{
_logger.LogWarning("⚠️ Kavenegar API not initialized, cannot send SMS");
return;
}
try
{
// Kavenegar Send is synchronous
await Task.Run(() =>
{
var result = _kavenegarApi.Send(
sender: _smsSettings.Sender,
receptor: mobile,
message: message);
_logger.LogInformation("📱 SMS sent successfully to {Mobile}, MessageId: {MessageId}", mobile, result.Messageid);
});
}
catch (Exception ex)
{
_logger.LogError(ex, "❌ Kavenegar error sending SMS to {Mobile}: {Message}", mobile, ex.Message);
throw;
}
}
/// <inheritdoc />
public async Task VerifyLookupAsync(string mobile, string token, string template = "Afrino")
{
if (!_smsSettings.Enabled)
{
_logger.LogInformation("SMS is disabled. Skipping VerifyLookup to {Mobile}", mobile);
return;
}
if (_kavenegarApi == null)
{
_logger.LogWarning("⚠️ Kavenegar API not initialized, cannot send VerifyLookup");
return;
}
try
{
// Kavenegar VerifyLookup is synchronous
await Task.Run(() =>
{
var result = _kavenegarApi.VerifyLookup(
receptor: mobile,
token: token,
template: template);
_logger.LogInformation("📱 VerifyLookup SMS sent successfully to {Mobile} with template {Template}, MessageId: {MessageId}",
mobile, template, result.Messageid);
});
}
catch (Exception ex)
{
_logger.LogError(ex, "❌ Kavenegar error sending VerifyLookup to {Mobile}: {Message}", mobile, ex.Message);
throw;
}
}
}
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.159</Version>
<Version>0.0.161</Version>
<DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
@@ -1,44 +0,0 @@
using CMSMicroservice.Protobuf.Protos.Configuration;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.ConfigurationCQ.Commands.SetConfigurationValue;
using CMSMicroservice.Application.ConfigurationCQ.Commands.DeactivateConfiguration;
using CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationByKey;
using CMSMicroservice.Application.ConfigurationCQ.Queries.GetAllConfigurations;
using CMSMicroservice.Application.ConfigurationCQ.Queries.GetConfigurationHistory;
namespace CMSMicroservice.WebApi.Services;
public class ConfigurationService : ConfigurationContract.ConfigurationContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public ConfigurationService(IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<Empty> CreateOrUpdateConfiguration(CreateOrUpdateConfigurationRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateOrUpdateConfigurationRequest, SetConfigurationValueCommand>(request, context);
}
public override async Task<Empty> DeactivateConfiguration(DeactivateConfigurationRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeactivateConfigurationRequest, DeactivateConfigurationCommand>(request, context);
}
public override async Task<GetConfigurationByKeyResponse> GetConfigurationByKey(GetConfigurationByKeyRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetConfigurationByKeyRequest, GetConfigurationByKeyQuery, GetConfigurationByKeyResponse>(request, context);
}
public override async Task<GetAllConfigurationsResponse> GetAllConfigurations(GetAllConfigurationsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllConfigurationsRequest, GetAllConfigurationsQuery, GetAllConfigurationsResponse>(request, context);
}
public override async Task<GetConfigurationHistoryResponse> GetConfigurationHistory(GetConfigurationHistoryRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetConfigurationHistoryRequest, GetConfigurationHistoryQuery, GetConfigurationHistoryResponse>(request, context);
}
}
@@ -84,7 +84,8 @@ public class DayaLoanCheckWorker
var processCommand = new ProcessDayaLoanApprovalCommand
{
UserId = user.Id,
ContractNumber = result.ContractNumber
ContractNumber = result.ContractNumber,
LockedWalletAmount = 0, // TODO: not needed for now
};
var processResult = await _mediator.Send(processCommand);
+2 -2
View File
@@ -37,8 +37,8 @@
"Sms": {
"Enabled": true,
"Provider": "Kavenegar",
"KavenegarApiKey": "YOUR_KAVENEGAR_API_KEY",
"Sender": "10008663"
"KavenegarApiKey": "497263626F32626A48685A6137524C4F78575A766E4C74694A556B79317648424964655030682B554545413D",
"Sender": "1000001110100"
},
"DayaPayment": {
"BaseUrl": "https://api.daya.ir",