diff --git a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs index d890d98..5847434 100644 --- a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs +++ b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs @@ -183,10 +183,9 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler c.UserId == user.Id && c.IsCurrentCycle) + .ToListAsync(cancellationToken); + + foreach (var prevCycle in previousCycles) + { + prevCycle.IsCurrentCycle = false; + } + + var maxCycleNumber = await _context.ClubMembershipCycles + .Where(c => c.ClubMembershipId == entity.Id) + .MaxAsync(c => (int?)c.CycleNumber, cancellationToken) ?? 0; + + var newCycle = new ClubMembershipCycle + { + UserId = user.Id, + ClubMembershipId = entity.Id, + CycleNumber = maxCycleNumber + 1, + PackagePurchasedAt = activationDate, + PurchaseMethod = user.PackagePurchaseMethod, + PackageAmount = SystemConstants.BasePackageAmount, + IsCurrentCycle = true + }; + + _context.ClubMembershipCycles.Add(newCycle); + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Created ClubMembershipCycle #{CycleNumber} for UserId {UserId}, MembershipId {MembershipId}", + newCycle.CycleNumber, + user.Id, + entity.Id + ); + // 7. ثبت تاریخچه var history = new ClubMembershipHistory { diff --git a/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs b/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs index ac83d4d..b7b2a0a 100644 --- a/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs +++ b/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs @@ -43,8 +43,14 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler w.WalletMode == WalletMode.Magic) + .Select(w => w.UserId) + .ToHashSetAsync(cancellationToken); + var activeClubMemberUserIds = await _context.ClubMemberships - .Where(c => c.IsActive) + .Where(c => c.IsActive && !magicModeUserIds.Contains(c.UserId)) .Select(c => c.UserId) .ToHashSetAsync(cancellationToken); @@ -256,11 +262,12 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler x.UserId == child.Id && x.IsActive, cancellationToken); + // اگر فرزند در این هفته پکیج خریده (Cycle جدید ساخته شده)، 1 امتیاز + // ⚠️ از ClubMembershipCycle.PackagePurchasedAt استفاده میکنیم (نه ActivatedAt که overwrite میشد) + var currentCycle = await _context.ClubMembershipCycles + .FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsCurrentCycle, cancellationToken); - if (membership?.ActivatedAt >= startDate && membership?.ActivatedAt <= endDate) + if (currentCycle?.PackagePurchasedAt >= startDate && currentCycle?.PackagePurchasedAt <= endDate) { count = 1; } diff --git a/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs b/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs index 8dccd92..64dcc7d 100644 --- a/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs @@ -38,6 +38,7 @@ public interface IApplicationDbContext DbSet PublicMessages { get; } DbSet ClubMemberships { get; } DbSet ClubMembershipHistories { get; } + DbSet ClubMembershipCycles { get; } DbSet ClubFeatures { get; } DbSet UserClubFeatures { get; } DbSet NetworkWeeklyBalances { get; } diff --git a/src/CMSMicroservice.Application/Common/Services/VatCalculator.cs b/src/CMSMicroservice.Application/Common/Services/VatCalculator.cs index 92e2cc9..e0d5a2e 100644 --- a/src/CMSMicroservice.Application/Common/Services/VatCalculator.cs +++ b/src/CMSMicroservice.Application/Common/Services/VatCalculator.cs @@ -6,14 +6,14 @@ namespace CMSMicroservice.Application.Common.Services; public static class VatCalculator { /// - /// نرخ VAT ایران - 9 درصد + /// نرخ VAT ایران - 10 درصد /// - public const decimal VAT_RATE = 0.09m; + public const decimal VAT_RATE = 0.10m; /// - /// نرخ VAT به صورت درصد (9) + /// نرخ VAT به صورت درصد (10) /// - public const int VAT_PERCENT = 9; + public const int VAT_PERCENT = 10; /// /// محاسبه VAT از مبلغ خالص diff --git a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandHandler.cs b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandHandler.cs index df174cc..b7d79c3 100644 --- a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandHandler.cs +++ b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyBasePackagePayment/VerifyBasePackagePaymentCommandHandler.cs @@ -133,7 +133,7 @@ public class VerifyBasePackagePaymentCommandHandler : IRequestHandler {NewBalance}, DiscountBalance: {OldDiscount} -> {NewDiscount}", @@ -157,7 +157,7 @@ public class VerifyBasePackagePaymentCommandHandler : IRequestHandler {NewBalance}", @@ -132,7 +132,7 @@ public class VerifyPackagePurchaseCommandHandler ChangeValue = order.Amount, CurrentNetworkBalance = wallet.NetworkBalance, ChangeNerworkValue = 0, - CurrentDiscountBalance = wallet.DiscountBalance - order.Amount, // قبل از شارژ DiscountBalance + CurrentDiscountBalance = wallet.DiscountBalance - (order.Amount * 2), // قبل از شارژ DiscountBalance ChangeDiscountValue = 0, IsIncrease = true, RefrenceId = transaction.Id @@ -148,7 +148,7 @@ public class VerifyPackagePurchaseCommandHandler CurrentNetworkBalance = wallet.NetworkBalance, ChangeNerworkValue = 0, CurrentDiscountBalance = wallet.DiscountBalance, - ChangeDiscountValue = order.Amount, + ChangeDiscountValue = order.Amount * 2, IsIncrease = true, RefrenceId = transaction.Id }; diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommand.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommand.cs new file mode 100644 index 0000000..3e05022 --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommand.cs @@ -0,0 +1,21 @@ +using CMSMicroservice.Application.Common.Interfaces; +using MediatR; + +namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet; + +/// +/// شروع شارژ کیف‌پول جادویی از طریق درگاه پرداخت +/// مبلغ واریزی × 2.5 به Balance اعتبار داده می‌شود +/// +public class ChargeMagicWalletCommand : IRequest +{ + /// + /// شناسه کاربر + /// + public long UserId { get; set; } + + /// + /// مبلغ واریزی واقعی (ریال) — اعتبار = مبلغ × 2.5 + /// + public long Amount { get; set; } +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandHandler.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandHandler.cs new file mode 100644 index 0000000..7c947bd --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandHandler.cs @@ -0,0 +1,174 @@ +using CMSMicroservice.Application.Common.Exceptions; +using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Common; +using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Entities.Payment; +using CMSMicroservice.Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet; + +public class ChargeMagicWalletCommandHandler + : IRequestHandler +{ + private readonly IApplicationDbContext _context; + private readonly IPaymentGatewayService _paymentGateway; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public ChargeMagicWalletCommandHandler( + IApplicationDbContext context, + IPaymentGatewayService paymentGateway, + IConfiguration configuration, + ILogger logger) + { + _context = context; + _paymentGateway = paymentGateway; + _configuration = configuration; + _logger = logger; + } + + public async Task Handle( + ChargeMagicWalletCommand request, + CancellationToken cancellationToken) + { + try + { + _logger.LogInformation( + "Initiating magic wallet charge for UserId: {UserId}, Amount: {Amount}", + request.UserId, + request.Amount + ); + + // 1. بررسی وجود کاربر + var user = await _context.Users + .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken); + + if (user == null) + { + _logger.LogWarning("User not found: {UserId}", request.UserId); + throw new NotFoundException(nameof(User), request.UserId); + } + + // 2. بررسی وجود کیف‌پول + var wallet = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == request.UserId, cancellationToken); + + if (wallet == null) + { + _logger.LogError("Wallet not found for UserId: {UserId}", request.UserId); + throw new NotFoundException("کیف پول کاربر یافت نشد"); + } + + // 3. بررسی حالت جادویی + if (wallet.WalletMode != WalletMode.Magic) + { + _logger.LogWarning( + "User {UserId} wallet is not in Magic mode (current: {Mode})", + request.UserId, + wallet.WalletMode + ); + throw new BadRequestException( + "شارژ جادویی فقط در حالت کیف‌پول جادویی امکان‌پذیر است" + ); + } + + // 4. بررسی سقف واریزی (per-cycle) + var remainingDeposit = SystemConstants.MagicWalletMaxDeposit - wallet.MagicTotalDeposited; + + if (remainingDeposit <= 0) + { + _logger.LogWarning( + "User {UserId} has reached magic deposit cap. TotalDeposited: {TotalDeposited}", + request.UserId, + wallet.MagicTotalDeposited + ); + throw new BadRequestException( + "سقف شارژ جادویی در این دور پر شده است" + ); + } + + if (request.Amount > remainingDeposit) + { + _logger.LogWarning( + "User {UserId} amount {Amount} exceeds remaining deposit cap {Remaining}", + request.UserId, + request.Amount, + remainingDeposit + ); + throw new BadRequestException( + $"مبلغ واریزی بیش از سقف باقیمانده است. حداکثر مبلغ قابل واریز: {remainingDeposit:N0} ریال" + ); + } + + // 5. ایجاد درخواست پرداخت + var cmsBaseUrl = _configuration["CmsBaseUrl"] ?? "https://localhost:32846"; + var callbackUrl = $"{cmsBaseUrl}/api/wallet/verify-magic-charge"; + + var paymentRequest = new PaymentRequest + { + Amount = request.Amount, + UserId = user.Id, + Mobile = user.Mobile ?? "", + CallbackUrl = callbackUrl, + Description = $"شارژ کیف‌پول جادویی - کاربر {user.Id}" + }; + + var paymentResult = await _paymentGateway.InitiatePaymentAsync(paymentRequest); + + if (!paymentResult.IsSuccess) + { + _logger.LogError( + "Payment gateway failed for magic charge UserId {UserId}: {ErrorMessage}", + user.Id, + paymentResult.ErrorMessage + ); + + throw new Exception($"خطا در ارتباط با درگاه پرداخت: {paymentResult.ErrorMessage}"); + } + + // 6. ثبت PaymentTransaction + var paymentTx = new PaymentTransaction + { + GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal", + MerchantId = _configuration["ZarinPal:MerchantId"] ?? "", + Amount = request.Amount, + CallbackUrl = callbackUrl, + Description = $"شارژ کیف‌پول جادویی - کاربر {user.Id}", + Mobile = user.Mobile, + UserId = user.Id, + RequestStatusCode = 100, + RequestStatusMessage = "Success", + Authority = paymentResult.RefId, + PaymentStatus = false + }; + + _context.PaymentTransactions.Add(paymentTx); + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Magic wallet charge initiated. UserId: {UserId}, Amount: {Amount}, " + + "RemainingCap: {Remaining}, RefId: {RefId}, PaymentTxId: {PaymentTxId}", + user.Id, + request.Amount, + remainingDeposit - request.Amount, + paymentResult.RefId, + paymentTx.Id + ); + + return paymentResult; + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error in ChargeMagicWalletCommand for UserId: {UserId}", + request.UserId + ); + throw; + } + } +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandValidator.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandValidator.cs new file mode 100644 index 0000000..175f12f --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/ChargeMagicWallet/ChargeMagicWalletCommandValidator.cs @@ -0,0 +1,19 @@ +using FluentValidation; + +namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet; + +public class ChargeMagicWalletCommandValidator : AbstractValidator +{ + public ChargeMagicWalletCommandValidator() + { + RuleFor(x => x.UserId) + .GreaterThan(0) + .WithMessage("شناسه کاربر باید بزرگتر از صفر باشد"); + + RuleFor(x => x.Amount) + .GreaterThanOrEqualTo(100_000) + .WithMessage("حداقل مبلغ شارژ جادویی ۱۰,۰۰۰ تومان است") + .LessThanOrEqualTo(1_000_000_000) + .WithMessage("حداکثر مبلغ شارژ جادویی ۱۰۰,۰۰۰,۰۰۰ تومان است"); + } +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommand.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommand.cs new file mode 100644 index 0000000..e7b9f69 --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommand.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge; + +/// +/// تأیید شارژ کیف‌پول جادویی — بعد از بازگشت از درگاه پرداخت +/// +public class VerifyMagicWalletChargeCommand : IRequest +{ + /// + /// کد Authority از درگاه + /// + public string Authority { get; set; } = string.Empty; + + /// + /// وضعیت برگشتی از درگاه (OK / NOK) + /// + public string Status { get; set; } = string.Empty; +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommandHandler.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommandHandler.cs new file mode 100644 index 0000000..855c657 --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommandHandler.cs @@ -0,0 +1,209 @@ +using CMSMicroservice.Application.Common.Exceptions; +using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Common; +using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Entities.Payment; +using CMSMicroservice.Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge; + +public class VerifyMagicWalletChargeCommandHandler + : IRequestHandler +{ + private readonly IApplicationDbContext _context; + private readonly IPaymentGatewayService _paymentGateway; + private readonly ILogger _logger; + + public VerifyMagicWalletChargeCommandHandler( + IApplicationDbContext context, + IPaymentGatewayService paymentGateway, + ILogger logger) + { + _context = context; + _paymentGateway = paymentGateway; + _logger = logger; + } + + public async Task Handle( + VerifyMagicWalletChargeCommand request, + CancellationToken cancellationToken) + { + try + { + _logger.LogInformation( + "Verifying magic wallet charge. Authority: {Authority}, Status: {Status}", + request.Authority, + request.Status + ); + + // 1. پیدا کردن PaymentTransaction از Authority + var paymentTx = await _context.PaymentTransactions + .FirstOrDefaultAsync(pt => pt.Authority == request.Authority, cancellationToken); + + if (paymentTx == null) + { + _logger.LogError("PaymentTransaction not found for Authority: {Authority}", request.Authority); + throw new NotFoundException("تراکنش پرداخت یافت نشد"); + } + + if (paymentTx.PaymentStatus) + { + _logger.LogWarning("PaymentTransaction already verified: {Authority}", request.Authority); + return true; // قبلاً تأیید شده + } + + var userId = paymentTx.UserId + ?? throw new BadRequestException("شناسه کاربر در تراکنش پرداخت یافت نشد"); + var depositAmount = paymentTx.Amount; // مبلغ واقعی واریزی (ریال) + + // 2. بررسی وضعیت برگشتی از درگاه + if (!string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogWarning( + "Magic charge cancelled by user. UserId: {UserId}, Authority: {Authority}", + userId, request.Authority + ); + + paymentTx.PaymentStatus = false; + paymentTx.VerificationStatusMessage = "پرداخت توسط کاربر لغو شد"; + await _context.SaveChangesAsync(cancellationToken); + + throw new BadRequestException("پرداخت توسط کاربر لغو شد"); + } + + // 3. Verify با درگاه (زرین‌پال نیاز به مبلغ دارد) + var amountInToman = depositAmount / 10m; + var verifyResult = await _paymentGateway.VerifyPaymentAsync( + request.Authority, + request.Status, + amountInToman, + cancellationToken + ); + + // آپدیت PaymentTransaction + paymentTx.PaymentStatus = verifyResult.IsSuccess; + paymentTx.VerificationStatusCode = verifyResult.VerificationCode; + paymentTx.VerificationStatusMessage = verifyResult.Message; + paymentTx.CardPan = verifyResult.CardPan; + paymentTx.CardHash = verifyResult.CardHash; + paymentTx.RefId = verifyResult.TrackingCode; + + if (!verifyResult.IsSuccess) + { + _logger.LogWarning( + "Magic wallet charge verification failed for UserId {UserId}: {Message}", + userId, + verifyResult.Message + ); + + await _context.SaveChangesAsync(cancellationToken); + throw new BadRequestException($"تراکنش ناموفق: {verifyResult.Message}"); + } + + // 4. بررسی کیف‌پول و حالت جادویی + var wallet = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == userId, cancellationToken); + + if (wallet == null) + { + _logger.LogError("Wallet not found for UserId: {UserId}", userId); + throw new NotFoundException("کیف پول کاربر یافت نشد"); + } + + if (wallet.WalletMode != WalletMode.Magic) + { + _logger.LogError( + "Wallet is not in Magic mode during verify. UserId: {UserId}, Mode: {Mode}", + userId, wallet.WalletMode + ); + throw new BadRequestException("کیف‌پول در حالت جادویی نیست"); + } + + // 5. محاسبه اعتبار ×2.5 + var creditAmount = (long)(depositAmount * SystemConstants.MagicWalletMultiplier); // مبلغ × 2.5 + var bonusAmount = creditAmount - depositAmount; // بونوس = مبلغ × 1.5 + + // 6. ثبت تراکنش واریز واقعی (MagicWalletDeposit) + var depositTransaction = new Transaction + { + Amount = depositAmount, + Description = $"شارژ کیف‌پول جادویی - واریز واقعی - کاربر {userId}", + PaymentStatus = PaymentStatus.Success, + PaymentDate = DateTime.UtcNow, + RefId = verifyResult.RefId, + Type = TransactionType.MagicWalletDeposit + }; + + _context.Transactions.Add(depositTransaction); + await _context.SaveChangesAsync(cancellationToken); + + // 7. ثبت تراکنش بونوس (MagicWalletBonus) + var bonusTransaction = new Transaction + { + Amount = bonusAmount, + Description = $"شارژ کیف‌پول جادویی - بونوس ×{SystemConstants.MagicWalletMultiplier - 1} - کاربر {userId}", + PaymentStatus = PaymentStatus.Success, + PaymentDate = DateTime.UtcNow, + RefId = $"MAGIC_BONUS_{depositTransaction.Id}", + Type = TransactionType.MagicWalletBonus + }; + + _context.Transactions.Add(bonusTransaction); + await _context.SaveChangesAsync(cancellationToken); + + // 8. شارژ Balance و آپدیت شمارنده‌ها + wallet.Balance += creditAmount; + wallet.MagicTotalDeposited += depositAmount; + wallet.MagicTotalCredited += creditAmount; + + // 9. ثبت WalletChangeLog (اجباری) + var walletLog = new UserWalletChangeLog + { + WalletId = wallet.Id, + CurrentBalance = wallet.Balance, + ChangeValue = creditAmount, + CurrentNetworkBalance = wallet.NetworkBalance, + ChangeNerworkValue = 0, + CurrentDiscountBalance = wallet.DiscountBalance, + ChangeDiscountValue = 0, + IsIncrease = true, + RefrenceId = depositTransaction.Id + }; + + _context.UserWalletChangeLogs.Add(walletLog); + + // 10. لینک PaymentTransaction به Transaction داخلی + paymentTx.TransactionId = depositTransaction.Id; + + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Magic wallet charged successfully. UserId: {UserId}, " + + "Deposit: {Deposit}, Credit: {Credit} (×{Multiplier}), Bonus: {Bonus}, " + + "TotalDeposited: {TotalDeposited}/{MaxDeposit}, NewBalance: {NewBalance}", + userId, + depositAmount, + creditAmount, + SystemConstants.MagicWalletMultiplier, + bonusAmount, + wallet.MagicTotalDeposited, + SystemConstants.MagicWalletMaxDeposit, + wallet.Balance + ); + + return true; + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error in VerifyMagicWalletChargeCommand. Authority: {Authority}", + request.Authority + ); + throw; + } + } +} diff --git a/src/CMSMicroservice.Domain/Common/SystemConstants.cs b/src/CMSMicroservice.Domain/Common/SystemConstants.cs index 34c35b1..e0b4b3d 100644 --- a/src/CMSMicroservice.Domain/Common/SystemConstants.cs +++ b/src/CMSMicroservice.Domain/Common/SystemConstants.cs @@ -92,6 +92,25 @@ public static class SystemConstants #endregion + #region Magic Wallet Settings + + /// + /// ضریب شارژ کیف‌پول جادویی — واریز × 2.5 = اعتبار + /// + public const decimal MagicWalletMultiplier = 2.5m; + + /// + /// سقف واریز در هر دور جادویی (ریال) — 100M تومان + /// + public const long MagicWalletMaxDeposit = 1_000_000_000; + + /// + /// سقف اعتبار در هر دور جادویی (ریال) — 250M تومان + /// + public const long MagicWalletMaxCredit = 2_500_000_000; + + #endregion + #region Shop Settings /// @@ -134,6 +153,11 @@ public static class SystemConstants ["System.MaintenanceMode"] = SystemMaintenanceMode, ["System.EnableAuditLog"] = SystemEnableAuditLog, + // Magic Wallet + ["MagicWallet.Multiplier"] = MagicWalletMultiplier, + ["MagicWallet.MaxDeposit"] = MagicWalletMaxDeposit, + ["MagicWallet.MaxCredit"] = MagicWalletMaxCredit, + // Shop ["Shop.VAT"] = ShopVAT, ["Shop.VATEnabled"] = ShopVATEnabled @@ -166,6 +190,11 @@ public static class SystemConstants ("System.MaintenanceMode", SystemMaintenanceMode, "حالت تعمیر و نگهداری سیستم"), ("System.EnableAuditLog", SystemEnableAuditLog, "فعال‌سازی لاگ تغییرات"), + // Magic Wallet + ("MagicWallet.Multiplier", MagicWalletMultiplier, "ضریب شارژ کیف‌پول جادویی (×2.5)"), + ("MagicWallet.MaxDeposit", MagicWalletMaxDeposit, "سقف واریز هر دور جادویی (ریال)"), + ("MagicWallet.MaxCredit", MagicWalletMaxCredit, "سقف اعتبار هر دور جادویی (ریال)"), + // Shop ("Shop.VAT", ShopVAT, "مالیات بر ارزش افزوده"), ("Shop.VATEnabled", ShopVATEnabled, "مالیات فعال است؟") diff --git a/src/CMSMicroservice.Domain/Entities/Club/ClubMembership.cs b/src/CMSMicroservice.Domain/Entities/Club/ClubMembership.cs index 66af6b7..089f6bf 100644 --- a/src/CMSMicroservice.Domain/Entities/Club/ClubMembership.cs +++ b/src/CMSMicroservice.Domain/Entities/Club/ClubMembership.cs @@ -55,4 +55,9 @@ public class ClubMembership : BaseAuditableEntity /// ClubMembershipHistory Collection Navigation Reference /// public virtual ICollection? ClubMembershipHistories { get; set; } + + /// + /// دوره‌های خرید پکیج — هر خرید پکیج یک Cycle جدید + /// + public virtual ICollection? Cycles { get; set; } } diff --git a/src/CMSMicroservice.Domain/Entities/Club/ClubMembershipCycle.cs b/src/CMSMicroservice.Domain/Entities/Club/ClubMembershipCycle.cs new file mode 100644 index 0000000..070f713 --- /dev/null +++ b/src/CMSMicroservice.Domain/Entities/Club/ClubMembershipCycle.cs @@ -0,0 +1,64 @@ +namespace CMSMicroservice.Domain.Entities.Club; + +/// +/// دوره خرید پکیج — هر بار خرید پکیج ۵۶M یک Cycle جدید ایجاد می‌شود. +/// برای حل مشکل overwrite شدن ClubMembership.ActivatedAt در محاسبه کمیسیون. +/// +public class ClubMembershipCycle : BaseAuditableEntity +{ + /// + /// شناسه کاربر + /// + public long UserId { get; set; } + + /// + /// User Navigation Property + /// + public virtual User User { get; set; } + + /// + /// شناسه عضویت باشگاه + /// + public long ClubMembershipId { get; set; } + + /// + /// ClubMembership Navigation Property + /// + public virtual ClubMembership ClubMembership { get; set; } + + /// + /// شماره دور (1, 2, 3, ...) + /// + public int CycleNumber { get; set; } + + /// + /// تاریخ خرید پکیج — این فیلد برای محاسبه کمیسیون هفتگی استفاده می‌شود + /// (به‌جای ClubMembership.ActivatedAt که overwrite می‌شد) + /// + public DateTime PackagePurchasedAt { get; set; } + + /// + /// تاریخ شروع حالت جادویی (وقتی Balance=0 شد) + /// + public DateTime? MagicStartedAt { get; set; } + + /// + /// تاریخ پایان حالت جادویی + /// + public DateTime? MagicCompletedAt { get; set; } + + /// + /// نحوه خرید پکیج در این دور + /// + public PackagePurchaseMethod PurchaseMethod { get; set; } + + /// + /// مبلغ پکیج (ریال) + /// + public long PackageAmount { get; set; } + + /// + /// آیا این دور فعلی است؟ فقط یک رکورد true می‌باشد + /// + public bool IsCurrentCycle { get; set; } +} diff --git a/src/CMSMicroservice.Domain/Entities/UserWallet.cs b/src/CMSMicroservice.Domain/Entities/UserWallet.cs index a24d42e..a3c1e27 100644 --- a/src/CMSMicroservice.Domain/Entities/UserWallet.cs +++ b/src/CMSMicroservice.Domain/Entities/UserWallet.cs @@ -18,7 +18,36 @@ public class UserWallet : BaseAuditableEntity /// موجودی تخفیف - فقط برای خرید از فروشگاه باشگاه مشتریان /// public long DiscountBalance { get; set; } - + + #region Magic Wallet + + /// + /// حالت کیف‌پول — Normal: کمیسیون فعال | Magic: شارژ ×2.5 بدون کمیسیون + /// + public WalletMode WalletMode { get; set; } = WalletMode.Normal; + + /// + /// مجموع واریزی واقعی در دور جادویی فعلی (ریال) — سقف: MagicWalletMaxDeposit + /// + public long MagicTotalDeposited { get; set; } + + /// + /// مجموع اعتبار داده‌شده در دور جادویی فعلی (ریال) — واریز × 2.5 + /// + public long MagicTotalCredited { get; set; } + + /// + /// زمان شروع دور جادویی فعلی + /// + public DateTime? MagicActivatedAt { get; set; } + + /// + /// زمان پایان دور جادویی فعلی + /// + public DateTime? MagicCompletedAt { get; set; } + + #endregion + //UserWalletChangeLog Collection Navigation Reference public virtual ICollection UserWalletChangeLogs { get; set; } } diff --git a/src/CMSMicroservice.Domain/Enums/TransactionType.cs b/src/CMSMicroservice.Domain/Enums/TransactionType.cs index 6846125..f93f9b6 100644 --- a/src/CMSMicroservice.Domain/Enums/TransactionType.cs +++ b/src/CMSMicroservice.Domain/Enums/TransactionType.cs @@ -26,4 +26,14 @@ public enum TransactionType /// خرید از فروشگاه تخفیف /// DiscountShopPurchase = 13, + + /// + /// واریز از درگاه به کیف‌پول جادویی (مبلغ واقعی) + /// + MagicWalletDeposit = 14, + + /// + /// بونوس داخلی کیف‌پول جادویی (مبلغ × 1.5) + /// + MagicWalletBonus = 15, } diff --git a/src/CMSMicroservice.Domain/Enums/WalletMode.cs b/src/CMSMicroservice.Domain/Enums/WalletMode.cs new file mode 100644 index 0000000..dcc99c0 --- /dev/null +++ b/src/CMSMicroservice.Domain/Enums/WalletMode.cs @@ -0,0 +1,13 @@ +namespace CMSMicroservice.Domain.Enums; + +/// +/// حالت کیف‌پول — Normal: کمیسیون فعال | Magic: شارژ ×2.5 بدون کمیسیون +/// +public enum WalletMode +{ + /// حالت عادی — کمیسیون و پورسانت فعال + Normal = 0, + + /// حالت جادویی — شارژ ×2.5، بدون کمیسیون + Magic = 1 +} diff --git a/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs b/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs index bf9b897..d772ffa 100644 --- a/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs @@ -109,6 +109,7 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext public DbSet ClubFeatures => Set(); public DbSet UserClubFeatures => Set(); public DbSet ClubMembershipHistories => Set(); + public DbSet ClubMembershipCycles => Set(); // Network public DbSet NetworkWeeklyBalances => Set(); diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Configurations/ClubMembershipCycleConfiguration.cs b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/ClubMembershipCycleConfiguration.cs new file mode 100644 index 0000000..19fb522 --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/ClubMembershipCycleConfiguration.cs @@ -0,0 +1,45 @@ +using CMSMicroservice.Domain.Entities.Club; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace CMSMicroservice.Infrastructure.Persistence.Configurations; + +public class ClubMembershipCycleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder 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.UserId).IsRequired(); + builder.Property(entity => entity.ClubMembershipId).IsRequired(); + builder.Property(entity => entity.CycleNumber).IsRequired(); + builder.Property(entity => entity.PackagePurchasedAt).IsRequired(); + builder.Property(entity => entity.MagicStartedAt).IsRequired(false); + builder.Property(entity => entity.MagicCompletedAt).IsRequired(false); + builder.Property(entity => entity.PurchaseMethod).IsRequired(); + builder.Property(entity => entity.PackageAmount).IsRequired(); + builder.Property(entity => entity.IsCurrentCycle) + .IsRequired() + .HasDefaultValue(false); + + // Relationships + builder.HasOne(entity => entity.User) + .WithMany() + .HasForeignKey(entity => entity.UserId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(entity => entity.ClubMembership) + .WithMany(cm => cm.Cycles) + .HasForeignKey(entity => entity.ClubMembershipId) + .OnDelete(DeleteBehavior.Restrict); + + // Indexes + builder.HasIndex(e => new { e.UserId, e.IsCurrentCycle }) + .HasDatabaseName("IX_ClubMembershipCycle_UserId_IsCurrentCycle"); + builder.HasIndex(e => e.PackagePurchasedAt) + .HasDatabaseName("IX_ClubMembershipCycle_PackagePurchasedAt"); + } +} diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserWalletConfiguration.cs b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserWalletConfiguration.cs index a39b681..6997722 100644 --- a/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserWalletConfiguration.cs +++ b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserWalletConfiguration.cs @@ -1,4 +1,5 @@ using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CMSMicroservice.Infrastructure.Persistence.Configurations; @@ -20,5 +21,18 @@ public class UserWalletConfiguration : IEntityTypeConfiguration builder.Property(entity => entity.NetworkBalance).IsRequired(true); builder.Property(entity => entity.DiscountBalance).IsRequired(true); + // Magic Wallet + builder.Property(entity => entity.WalletMode) + .IsRequired() + .HasDefaultValue(WalletMode.Normal); + builder.Property(entity => entity.MagicTotalDeposited) + .IsRequired() + .HasDefaultValue(0L); + builder.Property(entity => entity.MagicTotalCredited) + .IsRequired() + .HasDefaultValue(0L); + builder.Property(entity => entity.MagicActivatedAt).IsRequired(false); + builder.Property(entity => entity.MagicCompletedAt).IsRequired(false); + } } diff --git a/src/CMSMicroservice.Infrastructure/Services/Commission/OrmCommissionCalculationStrategy.cs b/src/CMSMicroservice.Infrastructure/Services/Commission/OrmCommissionCalculationStrategy.cs index ea6f36b..5db7dff 100644 --- a/src/CMSMicroservice.Infrastructure/Services/Commission/OrmCommissionCalculationStrategy.cs +++ b/src/CMSMicroservice.Infrastructure/Services/Commission/OrmCommissionCalculationStrategy.cs @@ -56,8 +56,14 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy } // دریافت همه کاربرانی که عضو فعال باشگاه هستند + // ⚠️ Magic Wallet: کاربرهایی که در حالت جادویی هستند از کمیسیون خارج میشن + var magicModeUserIds = await _context.UserWallets + .Where(w => w.WalletMode == WalletMode.Magic) + .Select(w => w.UserId) + .ToHashSetAsync(cancellationToken); + var activeClubMemberUserIds = await _context.ClubMemberships - .Where(c => c.IsActive) + .Where(c => c.IsActive && !magicModeUserIds.Contains(c.UserId)) .Select(c => c.UserId) .ToHashSetAsync(cancellationToken); @@ -364,10 +370,11 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy } var count = 0; - var membership = await _context.ClubMemberships - .FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsActive, cancellationToken); + // ⚠️ از ClubMembershipCycle.PackagePurchasedAt استفاده میکنیم (نه ActivatedAt که overwrite میشد) + var currentCycle = await _context.ClubMembershipCycles + .FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsCurrentCycle, cancellationToken); - if (membership?.ActivatedAt >= startDate && membership?.ActivatedAt <= endDate) + if (currentCycle?.PackagePurchasedAt >= startDate && currentCycle?.PackagePurchasedAt <= endDate) { count = 1; } diff --git a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj index 4d9ddd7..338a7dd 100644 --- a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj +++ b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj @@ -3,7 +3,7 @@ net9.0 enable enable - 0.0.180 + 0.0.181 None False False @@ -78,7 +78,7 @@ - $(PackageOutputPath)/$(PackageId).$(Version).nupkg + /home/masoud/Apps/project/FourSat/CMS/src/CMSMicroservice.Protobuf/bin/Debug/$(PackageId).$(Version).nupkg dotnet nuget push "$(NugetPackagePath)" --source foursat-hosted --api-key admin:87zH26nbqT --skip-duplicate --configfile "$(MSBuildThisFileDirectory)../NuGet.config" diff --git a/src/CMSMicroservice.Protobuf/Protos/userwallet.proto b/src/CMSMicroservice.Protobuf/Protos/userwallet.proto index 76c31f4..0d79f56 100644 --- a/src/CMSMicroservice.Protobuf/Protos/userwallet.proto +++ b/src/CMSMicroservice.Protobuf/Protos/userwallet.proto @@ -74,6 +74,20 @@ service UserWalletContract get: "/Customer/GetWithdrawalSettings" }; }; + + // ============= Magic Wallet Methods ============= + + rpc InitiateMagicCharge(InitiateMagicChargeRequest) returns (InitiateMagicChargeResponse){ + option (google.api.http) = { + post: "/Customer/InitiateMagicCharge" + body: "*" + }; + }; + rpc GetMagicWalletStatus(google.protobuf.Empty) returns (GetMagicWalletStatusResponse){ + option (google.api.http) = { + get: "/Customer/GetMagicWalletStatus" + }; + }; } message CreateNewUserWalletRequest { @@ -140,6 +154,7 @@ message GetCustomerWalletResponse int64 balance = 1; int64 network_balance = 2; int64 discount_balance = 3; + int32 wallet_mode = 4; // 0=Normal, 1=Magic } message GetCustomerWalletChangeLogRequest @@ -200,4 +215,29 @@ message CustomerWithdrawalModel message GetCustomerWithdrawalSettingsResponse { int64 min_withdrawal_amount = 1; +} + +// ============= Magic Wallet Messages ============= + +message InitiateMagicChargeRequest +{ + int64 amount = 1; // مبلغ واریزی واقعی (ریال) +} + +message InitiateMagicChargeResponse +{ + bool is_success = 1; + string gateway_url = 2; + string error_message = 3; +} + +message GetMagicWalletStatusResponse +{ + int32 wallet_mode = 1; // 0=Normal, 1=Magic + int64 magic_total_deposited = 2; + int64 magic_total_credited = 3; + int64 magic_max_deposit = 4; + int64 magic_remaining_deposit = 5; + int64 balance = 6; + google.protobuf.Timestamp magic_activated_at = 7; } \ No newline at end of file diff --git a/src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs b/src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs index 2a63b17..c9525ed 100644 --- a/src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs +++ b/src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs @@ -1,5 +1,6 @@ using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment; +using CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -143,4 +144,46 @@ public class PaymentCallbackController : ControllerBase $"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=error"); } } + + /// + /// Callback برای شارژ کیف‌پول جادویی — زرین‌پال بعد از پرداخت کاربر را اینجا برمی‌گرداند + /// + [HttpGet("/api/wallet/verify-magic-charge")] + public async Task MagicChargeCallback( + [FromQuery(Name = "Authority")] string? authority, + [FromQuery(Name = "Status")] string? status, + CancellationToken cancellationToken) + { + var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268"; + + _logger.LogInformation( + "Magic charge callback received: Authority={Authority}, Status={Status}", + authority, status); + + try + { + if (string.IsNullOrEmpty(authority)) + { + _logger.LogError("Magic charge callback: Authority is missing"); + return Redirect($"{frontOfficeBaseUrl}/magic-wallet?payment=error&reason=no-authority"); + } + + var result = await _sender.Send(new VerifyMagicWalletChargeCommand + { + Authority = authority, + Status = status ?? "NOK" + }, cancellationToken); + + _logger.LogInformation( + "Magic charge completed successfully. Authority={Authority}", + authority); + + return Redirect($"{frontOfficeBaseUrl}/magic-wallet?payment=success"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Magic charge callback error. Authority={Authority}", authority); + return Redirect($"{frontOfficeBaseUrl}/magic-wallet?payment=failed"); + } + } } diff --git a/src/CMSMicroservice.WebApi/Services/UserOrderService.cs b/src/CMSMicroservice.WebApi/Services/UserOrderService.cs index aacf89c..0dfd15d 100644 --- a/src/CMSMicroservice.WebApi/Services/UserOrderService.cs +++ b/src/CMSMicroservice.WebApi/Services/UserOrderService.cs @@ -6,7 +6,9 @@ using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrderHistory; using CMSMicroservice.Application.OrderManagementCQ.Commands.UpdateOrderStatus; using CMSMicroservice.Application.OrderManagementCQ.Commands.CancelOrderByAdmin; using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Common; using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Entities.Club; using CMSMicroservice.Domain.Entities.Order; using CMSMicroservice.Domain.Enums; using AppModels = CMSMicroservice.Application.Common.Models; @@ -272,7 +274,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase } // Calculate amounts - const decimal vatRate = 0.09m; + const decimal vatRate = 0.10m; long baseAmount = cartItems.Sum(c => c.Product.Price * c.Count); long vatAmount = (long)(baseAmount * vatRate); long totalAmount = baseAmount + vatAmount; @@ -334,7 +336,54 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase }; _context.UserWalletChangeLogs.Add(walletLog); - + + // ═══ Magic Wallet: Entry / Exit Trigger ═══ + if (wallet.Balance == 0) + { + var user = await _context.Users + .Include(u => u.ClubMembership) + .FirstOrDefaultAsync(u => u.Id == userId, context.CancellationToken); + + if (wallet.WalletMode == WalletMode.Magic + && wallet.MagicTotalDeposited >= SystemConstants.MagicWalletMaxDeposit) + { + // ── EXIT Magic Mode ── + // هر دو شرط: Balance=0 و سقف 100M پر شده + wallet.WalletMode = WalletMode.Normal; + wallet.MagicCompletedAt = DateTime.UtcNow; + + var currentCycle = await _context.ClubMembershipCycles + .FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, + context.CancellationToken); + if (currentCycle != null) + { + currentCycle.MagicCompletedAt = DateTime.UtcNow; + } + } + else if (wallet.WalletMode == WalletMode.Normal + && user != null + && user.PackagePurchaseMethod != PackagePurchaseMethod.None + && user.ClubMembership?.IsActive == true) + { + // ── ENTER Magic Mode ── + wallet.WalletMode = WalletMode.Magic; + wallet.MagicActivatedAt = DateTime.UtcNow; + wallet.MagicCompletedAt = null; + wallet.MagicTotalDeposited = 0; + wallet.MagicTotalCredited = 0; + + var currentCycle = await _context.ClubMembershipCycles + .FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, + context.CancellationToken); + if (currentCycle != null) + { + currentCycle.MagicStartedAt = DateTime.UtcNow; + } + } + // ⚠️ اگه Magic باشه و Balance=0 ولی سقف پر نشده → هنوز Magic! + // کاربر میتونه دوباره شارژ کنه + } + // Create order var order = new UserOrder { @@ -935,11 +984,11 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase public override Task GetVATRate(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context) { - // VAT Rate for Iran: 9% (نرخ مالیات بر ارزش افزوده ایران) + // VAT Rate for Iran: 10% (نرخ مالیات بر ارزش افزوده ایران) return Task.FromResult(new GetVATRateResponse { - VatRate = 0.09, - VatPercentage = 9, + VatRate = 0.10, + VatPercentage = 10, IsEnabled = true }); } diff --git a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs index 1f51dae..b48d611 100644 --- a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs +++ b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs @@ -3,13 +3,17 @@ using CMSMicroservice.WebApi.Common.Services; using CMSMicroservice.Application.UserWalletCQ.Commands.CreateNewUserWallet; using CMSMicroservice.Application.UserWalletCQ.Commands.UpdateUserWallet; using CMSMicroservice.Application.UserWalletCQ.Commands.DeleteUserWallet; +using CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet; using CMSMicroservice.Application.UserWalletCQ.Queries.GetUserWallet; using CMSMicroservice.Application.UserWalletCQ.Queries.GetAllUserWalletByFilter; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletChangeLog; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawals; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawalSettings; using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Common; +using CMSMicroservice.Domain.Enums; using Grpc.Core; +using Google.Protobuf.WellKnownTypes; using Microsoft.EntityFrameworkCore; using System.Linq; @@ -64,11 +68,15 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase var walletQuery = new GetUserWalletQuery { Id = userId }; var wallet = await _sender.Send(walletQuery, context.CancellationToken); + var walletEntity = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == userId, context.CancellationToken); + return new GetCustomerWalletResponse { Balance = wallet.Balance, NetworkBalance = wallet.NetworkBalance, - DiscountBalance = wallet.DiscountBalance + DiscountBalance = wallet.DiscountBalance, + WalletMode = (int)(walletEntity?.WalletMode ?? WalletMode.Normal) }; } @@ -141,6 +149,57 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase return new Google.Protobuf.WellKnownTypes.Empty(); } + // ============= Magic Wallet Methods ============= + + public override async Task InitiateMagicCharge( + InitiateMagicChargeRequest request, ServerCallContext context) + { + var userId = GetCurrentUserId(); + + var result = await _sender.Send(new ChargeMagicWalletCommand + { + UserId = userId, + Amount = request.Amount + }, context.CancellationToken); + + return new InitiateMagicChargeResponse + { + IsSuccess = result.IsSuccess, + GatewayUrl = result.GatewayUrl ?? "", + ErrorMessage = result.ErrorMessage ?? "" + }; + } + + public override async Task GetMagicWalletStatus( + Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context) + { + var userId = GetCurrentUserId(); + + var wallet = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == userId, context.CancellationToken); + + if (wallet == null) + throw new RpcException(new Status(StatusCode.NotFound, "کیف پول یافت نشد")); + + var response = new GetMagicWalletStatusResponse + { + WalletMode = (int)wallet.WalletMode, + MagicTotalDeposited = wallet.MagicTotalDeposited, + MagicTotalCredited = wallet.MagicTotalCredited, + MagicMaxDeposit = SystemConstants.MagicWalletMaxDeposit, + MagicRemainingDeposit = Math.Max(0, SystemConstants.MagicWalletMaxDeposit - wallet.MagicTotalDeposited), + Balance = wallet.Balance + }; + + if (wallet.MagicActivatedAt.HasValue) + { + response.MagicActivatedAt = Timestamp.FromDateTime( + DateTime.SpecifyKind(wallet.MagicActivatedAt.Value, DateTimeKind.Utc)); + } + + return response; + } + private long GetCurrentUserId() { if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0)