F2-F7: نوتیفیکیشن‌ها+نام‌پکیج، ستون پکیج در CSV، مقادیر داینامیک کیف‌پول جادویی، ولیدیتور SystemConstants
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m29s

F2: اضافه شدن packageName به SmsTemplates، IUserNotificationService، UserNotificationService
    - DayaLoan، ClubActivated، CommissionDeposited حالا نام پکیج را نشان می‌دهند
F4: اضافه شدن ستون پکیج به CSV خروجی‌ها
    - commission.proto: package_name در WithdrawalRequestModel
    - manualpayment.proto: package_name در ManualPaymentModel
    - کوئری‌هندلرها UserPackagePurchases لوکاپ اضافه شد
F6: مقادیر داینامیک کیف‌پول جادویی از پکیج
    - userwallet.proto: magic_multiplier + magic_max_credit
    - UserWalletService: پاپیولیت فیلدهای جدید + فالبک به SystemConstants
F7: ولیدیتورها از SystemConstants استفاده می‌کنند
    - WalletMaxSafeAmount (10B ریال) به عنوان حصار ایمنی
    - MagicWalletMinCharge، DiscountWalletMinCharge ثابت‌های مرکزی
    - سقف واقعی per-package در هندلرها اعمال می‌شود
Proto: v0.0.189
This commit is contained in:
masoodafar-web
2026-02-27 08:47:46 +03:30
parent e5bc3a952a
commit 61b7e4f8f2
18 changed files with 129 additions and 33 deletions
@@ -51,6 +51,15 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
.PaginatedListAsync(paginationState: request.PaginationState)
.ToListAsync(cancellationToken);
// دریافت PackageName از آخرین خرید پکیج کاربران
var userIds = models.Select(x => x.UserId).Distinct().ToList();
var userPackageNames = await _context.UserPackagePurchases
.Where(p => userIds.Contains(p.UserId))
.Include(p => p.Package)
.GroupBy(p => p.UserId)
.Select(g => new { UserId = g.Key, PackageName = g.OrderByDescending(x => x.PurchasedAt).First().Package.Title })
.ToDictionaryAsync(x => x.UserId, x => x.PackageName, cancellationToken);
var result = models.Select(x => new WithdrawalRequestModel
{
Id = x.Id,
@@ -69,7 +78,8 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
BankReferenceId = x.BankReferenceId,
BankTrackingCode = x.BankTrackingCode,
PaymentFailureReason = x.PaymentFailureReason,
Created = x.Created
Created = x.Created,
PackageName = userPackageNames.GetValueOrDefault(x.UserId)
}).ToList();
return new GetWithdrawalRequestsResponseDto
@@ -25,4 +25,5 @@ public class WithdrawalRequestModel
public string? BankTrackingCode { get; set; }
public string? PaymentFailureReason { get; set; }
public DateTime Created { get; set; }
public string? PackageName { get; set; }
}
@@ -34,14 +34,16 @@ public interface IUserNotificationService
Task SendCommissionReceivedNotificationAsync(
long userId,
decimal amount,
int weekNumber,
int weekNumber,
string? packageName = null,
CancellationToken cancellationToken = default);
/// <summary>
/// ارسال اعلان فعال‌سازی عضویت باشگاه
/// </summary>
Task SendClubActivationNotificationAsync(
long userId,
long userId,
string? packageName = null,
CancellationToken cancellationToken = default);
/// <summary>
@@ -49,6 +51,7 @@ public interface IUserNotificationService
/// </summary>
Task SendPayoutErrorNotificationAsync(
long userId,
string errorMessage,
string errorMessage,
string? packageName = null,
CancellationToken cancellationToken = default);
}
@@ -122,6 +122,7 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
contract.IsProcessed = true;
result.WasProcessed = true;
result.NewWalletBalance = processResult.NewBalance;
result.PackageName = processResult.PackageName;
result.Message = "وام پردازش و کیف پول شارژ شد";
processedCount++;
@@ -181,7 +182,7 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
/// <summary>
/// پردازش مالی وام دایا
/// </summary>
private async Task<(long NewBalance, long TransactionId)> ProcessDayaLoanAsync(
private async Task<(long NewBalance, long TransactionId, string PackageName)> ProcessDayaLoanAsync(
User user,
string contractNumber,
CancellationToken cancellationToken)
@@ -279,25 +280,25 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
await _context.SaveChangesAsync(cancellationToken);
// 8. ارسال SMS
await SendDayaLoanSmsAsync(user);
await SendDayaLoanSmsAsync(user, package.Title);
return (wallet.Balance, transaction.Id);
return (wallet.Balance, transaction.Id, package.Title);
}
/// <summary>
/// ارسال SMS اطلاع‌رسانی
/// </summary>
private async Task SendDayaLoanSmsAsync(User user)
private async Task SendDayaLoanSmsAsync(User user, string packageName)
{
try
{
var userName = SmsTemplates.GetUserName(user.FirstName+" "+user.LastName);
var message = SmsTemplates.DayaLoanReceived(userName);
var message = SmsTemplates.DayaLoanReceived(userName, packageName);
await _smsService.SendAsync(user.Mobile, message);
// ارسال SMS به ادمین برای اطلاع
var adminMessage = $"وام دایا دریافت شد\nکاربر: {user.FirstName} {user.LastName}\nموبایل: {user.Mobile}\nکدملی: {user.NationalCode}";
var adminMessage = $"وام دایا دریافت شد\nکاربر: {user.FirstName} {user.LastName}\nموبایل: {user.Mobile}\nکدملی: {user.NationalCode}\nپکیج: {packageName}";
await _smsService.SendAsync("09199877503", adminMessage);
_logger.LogInformation("Daya loan SMS sent to User {UserId}", user.Id);
@@ -63,6 +63,11 @@ public class DayaLoanProcessResult
/// </summary>
public long? NewWalletBalance { get; set; }
/// <summary>
/// نام پکیج
/// </summary>
public string? PackageName { get; set; }
/// <summary>
/// پیام
/// </summary>
@@ -1,3 +1,4 @@
using CMSMicroservice.Domain.Common;
using FluentValidation;
namespace CMSMicroservice.Application.ManualPaymentCQ.Commands.CreateManualPayment;
@@ -10,11 +11,12 @@ public class CreateManualPaymentCommandValidator : AbstractValidator<CreateManua
.GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
// حصار ایمنی — مبلغ واقعی از پکیج کاربر خوانده می‌شود
RuleFor(x => x.Amount)
.GreaterThan(0)
.WithMessage("مبلغ باید بزرگتر از صفر باشد")
.LessThanOrEqualTo(1_000_000_000)
.WithMessage("مبلغ نمی‌تواند بیشتر از 1 میلیارد ریال باشد");
.LessThanOrEqualTo(SystemConstants.WalletMaxSafeAmount)
.WithMessage("مبلغ وارد شده بیش از حد مجاز است");
RuleFor(x => x.Type)
.IsInEnum()
@@ -69,6 +69,14 @@ public class GetAllManualPaymentsQueryHandler
// Pagination
var skip = (request.PageNumber - 1) * request.PageSize;
// دریافت PackageName از آخرین خرید پکیج کاربر
var userPackageNames = await _context.UserPackagePurchases
.Include(p => p.Package)
.GroupBy(p => p.UserId)
.Select(g => new { UserId = g.Key, PackageName = g.OrderByDescending(x => x.PurchasedAt).First().Package.Title })
.ToDictionaryAsync(x => x.UserId, x => x.PackageName, cancellationToken);
var data = await query
.Skip(skip)
.Take(request.PageSize)
@@ -95,6 +103,13 @@ public class GetAllManualPaymentsQueryHandler
Created = m.Created
})
.ToListAsync(cancellationToken);
// تکمیل PackageName
foreach (var item in data)
{
if (userPackageNames.TryGetValue(item.UserId, out var pkgName))
item.PackageName = pkgName;
}
var metaData = new MetaData
{
@@ -30,4 +30,5 @@ public class ManualPaymentDto
public string? RejectionReason { get; set; }
public long? TransactionId { get; set; }
public DateTime Created { get; set; }
public string? PackageName { get; set; }
}
@@ -1,3 +1,4 @@
using CMSMicroservice.Domain.Common;
using FluentValidation;
namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeDiscountWallet;
@@ -10,10 +11,11 @@ public class ChargeDiscountWalletCommandValidator : AbstractValidator<ChargeDisc
.GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
// حصار ایمنی — سقف تخفیفی فعلاً سراسری است
RuleFor(x => x.Amount)
.GreaterThanOrEqualTo(10_000)
.WithMessage("حداقل مبلغ شارژ 10,000 تومان است")
.LessThanOrEqualTo(1_000_000_000)
.WithMessage("حداکثر مبلغ شارژ 1,000,000,000 تومان است");
.GreaterThanOrEqualTo(SystemConstants.DiscountWalletMinCharge)
.WithMessage("حداقل مبلغ شارژ ۱,۰۰۰ تومان است")
.LessThanOrEqualTo(SystemConstants.WalletMaxSafeAmount)
.WithMessage("مبلغ وارد شده بیش از حد مجاز است");
}
}
@@ -1,3 +1,4 @@
using CMSMicroservice.Domain.Common;
using FluentValidation;
namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet;
@@ -10,10 +11,11 @@ public class ChargeMagicWalletCommandValidator : AbstractValidator<ChargeMagicWa
.GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
// حصار ایمنی — سقف واقعی per-package در هندلر اعمال می‌شود
RuleFor(x => x.Amount)
.GreaterThanOrEqualTo(100_000)
.GreaterThanOrEqualTo(SystemConstants.MagicWalletMinCharge)
.WithMessage("حداقل مبلغ شارژ جادویی ۱۰,۰۰۰ تومان است")
.LessThanOrEqualTo(1_000_000_000)
.WithMessage("حداکثر مبلغ شارژ جادویی ۱۰۰,۰۰۰,۰۰۰ تومان است");
.LessThanOrEqualTo(SystemConstants.WalletMaxSafeAmount)
.WithMessage("مبلغ وارد شده بیش از حد مجاز است");
}
}
@@ -8,14 +8,18 @@ public static class SmsTemplates
/// <summary>
/// پیام دریافت اعتبار دایا
/// </summary>
public static string DayaLoanReceived(string userName) =>
$"{userName} عزیز، تامین اعتبار شما انجام شد. شما می‌توانید با مراجعه به سامانه کارابازار فرایند فعال‌سازی خود را ادامه دهید. با تشکر از حسن انتخاب شما";
public static string DayaLoanReceived(string userName, string? packageName = null) =>
packageName != null
? $"{userName} عزیز، تامین اعتبار {packageName} شما انجام شد. شما می‌توانید با مراجعه به سامانه کارابازار فرایند فعال‌سازی خود را ادامه دهید. با تشکر از حسن انتخاب شما"
: $"{userName} عزیز، تامین اعتبار شما انجام شد. شما می‌توانید با مراجعه به سامانه کارابازار فرایند فعال‌سازی خود را ادامه دهید. با تشکر از حسن انتخاب شما";
/// <summary>
/// پیام فعال‌سازی باشگاه مشتریان
/// </summary>
public static string ClubActivated(string userName) =>
$"{userName} گرامی، عضویت شما در باشگاه مشتریان کارابازار با موفقیت فعال شد. با تشکر از حسن انتخاب شما";
public static string ClubActivated(string userName, string? packageName = null) =>
packageName != null
? $"{userName} گرامی، عضویت شما در باشگاه مشتریان کارابازار با {packageName} فعال شد. با تشکر از حسن انتخاب شما"
: $"{userName} گرامی، عضویت شما در باشگاه مشتریان کارابازار با موفقیت فعال شد. با تشکر از حسن انتخاب شما";
/// <summary>
/// پیام خرید پکیج
@@ -26,8 +30,10 @@ public static class SmsTemplates
/// <summary>
/// پیام واریز کمیسیون
/// </summary>
public static string CommissionDeposited(string userName, long amount) =>
$"{userName} گرامی، مبلغ {amount:N0} ریال بابت کمیسیون به کیف پول شما واریز شد.";
public static string CommissionDeposited(string userName, long amount, string? packageName = null) =>
packageName != null
? $"{userName} گرامی، مبلغ {amount:N0} ریال بابت کمیسیون {packageName} به کیف پول شما واریز شد."
: $"{userName} گرامی، مبلغ {amount:N0} ریال بابت کمیسیون به کیف پول شما واریز شد.";
/// <summary>
/// پیام برداشت موفق
@@ -66,6 +66,41 @@ public static class SystemConstants
/// </summary>
public const long MagicWalletEntryThreshold = 1_000_000;
/// <summary>
/// مقدار پیش‌فرض ضریب جادویی وقتی پکیج یافت نشود (بازگشت به پایه)
/// </summary>
public const double MagicWalletDefaultMultiplier = 2.5;
/// <summary>
/// مقدار پیش‌فرض حداکثر واریز جادویی وقتی پکیج یافت نشود (ریال)
/// </summary>
public const long MagicWalletDefaultMaxDeposit = 1_000_000_000;
/// <summary>
/// مقدار پیش‌فرض حداکثر اعتبار جادویی وقتی پکیج یافت نشود (ریال)
/// </summary>
public const long MagicWalletDefaultMaxCredit = 2_500_000_000;
#endregion
#region Wallet Validation Settings
/// <summary>
/// حداکثر مبلغ مجاز در ولیدیتور (حصار ایمنی — مقدار واقعی از پکیج خوانده می‌شود).
/// ۱۰ میلیارد ریال = ۱ میلیارد تومان
/// </summary>
public const long WalletMaxSafeAmount = 10_000_000_000;
/// <summary>
/// حداقل مبلغ شارژ جادویی (ریال) — ۱۰۰٬۰۰۰ ریال = ۱۰ هزار تومان
/// </summary>
public const long MagicWalletMinCharge = 100_000;
/// <summary>
/// حداقل مبلغ شارژ تخفیفی (ریال) — ۱۰٬۰۰۰ ریال = ۱ هزار تومان
/// </summary>
public const long DiscountWalletMinCharge = 10_000;
#endregion
#region Shop Settings
@@ -49,6 +49,7 @@ public class UserNotificationService : IUserNotificationService
long userId,
decimal amount,
int weekNumber,
string? packageName = null,
CancellationToken cancellationToken = default)
{
_logger.LogInformation(
@@ -69,14 +70,15 @@ public class UserNotificationService : IUserNotificationService
if (string.IsNullOrEmpty(userFullName)) userFullName = "کاربر عزیز";
var formattedAmount = amount.ToString("N0", new System.Globalization.CultureInfo("fa-IR"));
var packageInfo = !string.IsNullOrEmpty(packageName) ? $" ({packageName})" : "";
// Send Email
if (_emailSettings.Enabled && !string.IsNullOrEmpty(user.Email))
{
var emailSubject = $"واریز کمیسیون هفته {weekNumber}";
var emailSubject = $"واریز کمیسیون هفته {weekNumber}{packageInfo}";
var emailBody = "<div dir='rtl' style='font-family: Tahoma, Arial; text-align: right;'>" +
$"<h2>سلام {userFullName}</h2>" +
$"<p>کمیسیون هفته {weekNumber} شما به مبلغ <strong>{formattedAmount} ریال</strong> به کیف پول شما واریز شد.</p>" +
$"<p>کمیسیون هفته {weekNumber} شما{packageInfo} به مبلغ <strong>{formattedAmount} ریال</strong> به کیف پول شما واریز شد.</p>" +
"<p>از اعتماد شما سپاسگزاریم.</p>" +
"<hr/>" +
"<p style='color: #666; font-size: 12px;'>FourSat - سیستم مدیریت باشگاه مشتریان</p>" +
@@ -95,7 +97,7 @@ public class UserNotificationService : IUserNotificationService
{
await SendSmsAsync(
phoneNumber: user.Mobile,
message: $"سلام {userFullName}\nکمیسیون هفته {weekNumber} شما به مبلغ {formattedAmount} ریال واریز شد.\nFourSat",
message: $"سلام {userFullName}\nکمیسیون هفته {weekNumber} شما{packageInfo} به مبلغ {formattedAmount} ریال واریز شد.\nFourSat",
cancellationToken: cancellationToken);
}
@@ -109,6 +111,7 @@ public class UserNotificationService : IUserNotificationService
public async Task SendClubActivationNotificationAsync(
long userId,
string? packageName = null,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("🎉 Sending club activation notification: User={UserId}", userId);
@@ -120,6 +123,7 @@ public class UserNotificationService : IUserNotificationService
var userFullName = $"{user.FirstName} {user.LastName}".Trim();
if (string.IsNullOrEmpty(userFullName)) userFullName = "کاربر عزیز";
var packageInfo = !string.IsNullOrEmpty(packageName) ? $" با {packageName}" : "";
// Send Email
if (_emailSettings.Enabled && !string.IsNullOrEmpty(user.Email))
@@ -127,7 +131,7 @@ public class UserNotificationService : IUserNotificationService
var emailSubject = "فعال‌سازی باشگاه مشتریان FourSat";
var emailBody = "<div dir='rtl' style='font-family: Tahoma, Arial; text-align: right;'>" +
$"<h2>تبریک {userFullName}!</h2>" +
"<p>عضویت شما در <strong>باشگاه مشتریان FourSat</strong> با موفقیت فعال شد.</p>" +
$"<p>عضویت شما در <strong>باشگاه مشتریان FourSat</strong>{packageInfo} با موفقیت فعال شد.</p>" +
"<p>از این پس می‌توانید از مزایای ویژه باشگاه بهره‌مند شوید.</p>" +
"<hr/>" +
"<p style='color: #666; font-size: 12px;'>FourSat - سیستم مدیریت باشگاه مشتریان</p>" +
@@ -146,7 +150,7 @@ public class UserNotificationService : IUserNotificationService
{
await SendSmsAsync(
phoneNumber: user.Mobile,
message: $"تبریک! عضویت شما در باشگاه مشتریان FourSat فعال شد.",
message: $"تبریک! عضویت شما در باشگاه مشتریان FourSat{packageInfo} فعال شد.",
cancellationToken: cancellationToken);
}
}
@@ -159,6 +163,7 @@ public class UserNotificationService : IUserNotificationService
public async Task SendPayoutErrorNotificationAsync(
long userId,
string errorMessage,
string? packageName = null,
CancellationToken cancellationToken = default)
{
_logger.LogWarning(
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.188</Version>
<Version>0.0.189</Version>
<DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
@@ -531,6 +531,7 @@ message WithdrawalRequestModel
string bank_reference_id = 15;
string bank_tracking_code = 16;
string payment_failure_reason = 17;
google.protobuf.StringValue package_name = 18; // نام پکیج کاربر
}
// ============ Week Definitions (for dropdowns) ============
@@ -138,6 +138,7 @@ message ManualPaymentModel
google.protobuf.Timestamp created = 19;
google.protobuf.StringValue image_path = 20;
google.protobuf.Int64Value image_document_id = 21;
google.protobuf.StringValue package_name = 22; // نام پکیج کاربر
}
message ProcessManualMembershipPaymentRequest
@@ -251,6 +251,8 @@ message GetMagicWalletStatusResponse
int64 balance = 6;
google.protobuf.Timestamp magic_activated_at = 7;
int32 purchase_cycle_count = 8; // تعداد دورهای خرید پکیج (0 = هنوز هیچ دوری تکمیل نشده)
double magic_multiplier = 9; // ضریب جادویی (مثلاً 2.5)
int64 magic_max_credit = 10; // حداکثر اعتبار (ریال)
}
// ============= Discount Wallet Messages =============
@@ -233,7 +233,9 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
var package = currentCycle != null
? await _context.Packages.FirstOrDefaultAsync(p => p.Id == currentCycle.PackageId, context.CancellationToken)
: await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, context.CancellationToken);
var magicMaxDeposit = package?.MagicWalletMaxDeposit ?? 1_000_000_000;
var magicMaxDeposit = package?.MagicWalletMaxDeposit ?? SystemConstants.MagicWalletDefaultMaxDeposit;
var magicMultiplier = package != null ? (double)package.MagicWalletMultiplier : SystemConstants.MagicWalletDefaultMultiplier;
var magicMaxCredit = package?.MagicWalletMaxCredit ?? SystemConstants.MagicWalletDefaultMaxCredit;
var response = new GetMagicWalletStatusResponse
{
@@ -243,7 +245,9 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
MagicMaxDeposit = magicMaxDeposit,
MagicRemainingDeposit = Math.Max(0, magicMaxDeposit - wallet.MagicTotalDeposited),
Balance = wallet.Balance,
PurchaseCycleCount = cycleCount
PurchaseCycleCount = cycleCount,
MagicMultiplier = magicMultiplier,
MagicMaxCredit = magicMaxCredit
};
if (wallet.MagicActivatedAt.HasValue)