Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 8m48s

# Conflicts:
#	.gitea/workflows/kub-deploy.yml
#	k8s/staging/cms-deployment.yaml
#	src/CMSMicroservice.Application/DiscountShopCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommandHandler.cs
#	src/CMSMicroservice.Application/DiscountShopCQ/Commands/PlaceOrder/PlaceOrderCommandHandler.cs
#	src/CMSMicroservice.Application/WalletCQ/Commands/VerifyDiscountWalletCharge/VerifyDiscountWalletChargeCommandHandler.cs
#	src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommandHandler.cs
#	src/CMSMicroservice.Infrastructure/Services/Payment/ZarinPalPaymentService.cs
#	src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs
#	src/CMSMicroservice.WebApi/appsettings.Production.json
#	src/CMSMicroservice.WebApi/appsettings.Staging.json
#	src/CMSMicroservice.WebApi/appsettings.json
This commit is contained in:
masoodafar-web
2026-02-28 04:51:57 +03:30
209 changed files with 15064 additions and 2696 deletions
@@ -68,7 +68,9 @@ public class CommissionProfile : IRegister
.Map(dest => dest.WithdrawnAt, src => src.WithdrawnAt.HasValue
? Timestamp.FromDateTime(src.WithdrawnAt.Value.ToUniversalTime())
: null)
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created));
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created))
.Map(dest => dest.PackageId, src => src.PackageId)
.Map(dest => dest.PackageTitle, src => src.PackageTitle);
// GetWeekDefinitions Request Mapping
config.NewConfig<GetWeekDefinitionsRequest, GetWeekDefinitionsQuery>()
@@ -127,7 +129,9 @@ public class CommissionProfile : IRegister
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.CalculatedAt.Value, DateTimeKind.Utc))
: null)
.Map(dest => dest.IsExpired, src => src.IsExpired)
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created));
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created))
.Map(dest => dest.PackageId, src => src.PackageId)
.Map(dest => dest.PackageTitle, src => src.PackageTitle);
// GetMyCommissionPayouts Request Mapping
config.NewConfig<GetMyCommissionPayoutsRequest, GetMyCommissionPayoutsQuery>()
@@ -135,6 +139,7 @@ public class CommissionProfile : IRegister
? (CommissionPayoutStatus?)src.Status.Value
: null)
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId != null ? src.WeekDefinitionId.Value : (long?)null)
.Map(dest => dest.PackageId, src => src.PackageId != null ? src.PackageId.Value : (long?)null)
.Map(dest => dest.PaginationState, src => new PaginationState
{
PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
@@ -160,12 +165,15 @@ public class CommissionProfile : IRegister
.Map(dest => dest.CalculatedDate, src => src.CalculatedDate.HasValue
? Timestamp.FromDateTime(src.CalculatedDate.Value.ToUniversalTime())
: null)
.Map(dest => dest.DatePersian, src => src.DatePersian);
.Map(dest => dest.DatePersian, src => src.DatePersian)
.Map(dest => dest.PackageId, src => src.PackageId)
.Map(dest => dest.PackageTitle, src => src.PackageTitle);
// GetMyWeeklyBalances Request Mapping
config.NewConfig<GetMyWeeklyBalancesRequest, GetMyWeeklyBalancesQuery>()
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId != null ? src.WeekDefinitionId.Value : (long?)null)
.Map(dest => dest.OnlyActive, src => src.OnlyActive)
.Map(dest => dest.PackageId, src => src.PackageId != null ? src.PackageId.Value : (long?)null)
.Map(dest => dest.PaginationState, src => new PaginationState
{
PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
@@ -199,6 +207,8 @@ public class CommissionProfile : IRegister
.Map(dest => dest.IsExpired, src => src.IsExpired)
.Map(dest => dest.CalculatedAt, src => src.CalculatedAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.CalculatedAt.Value, DateTimeKind.Utc))
: null);
: null)
.Map(dest => dest.PackageId, src => src.PackageId)
.Map(dest => dest.PackageTitle, src => src.PackageTitle);
}
}
@@ -56,7 +56,7 @@ public class NetworkMembershipProfile : IRegister
.Map(dest => dest.MaxNetworkDepth, src => src.MaxNetworkDepth)
.Map(dest => dest.HasReceivedDayaCredit, src => src.HasReceivedDayaCredit)
.Map(dest => dest.PackagePurchaseMethod, src => (int)src.PackagePurchaseMethod)
.Map(dest => dest.HasPurchasedGoldenPackage, src => src.HasPurchasedGoldenPackage)
.Map(dest => dest.HasPurchasedPackage, src => src.HasPurchasedPackage)
.Map(dest => dest.TotalEarnedCommission, src => (double)src.TotalEarnedCommission)
.Map(dest => dest.TotalPaidCommission, src => (double)src.TotalPaidCommission)
.Map(dest => dest.PendingCommission, src => (double)src.PendingCommission)
@@ -1,7 +1,3 @@
using CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
using CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
using CMSMicroservice.Application.PackageCQ.Commands.InitiateBasePackagePayment;
using CMSMicroservice.Application.PackageCQ.Commands.VerifyBasePackagePayment;
using CMSMicroservice.Application.PackageCQ.Queries.GetUserPackageStatus;
using CMSMicroservice.Protobuf.Protos.Package;
using Google.Protobuf.WellKnownTypes;
@@ -12,33 +8,6 @@ public class PackageProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// PurchaseGoldenPackage
config.NewConfig<PurchaseGoldenPackageRequest, PurchaseGoldenPackageCommand>()
.Map(dest => dest.UserId, src => src.UserId)
.Map(dest => dest.PackageId, src => src.PackageId)
.Map(dest => dest.ReturnUrl, src => src.ReturnUrl);
config.NewConfig<PurchaseGoldenPackageResponseDto, PurchaseGoldenPackageResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.PaymentGatewayUrl, src => src.PaymentGatewayUrl)
.Map(dest => dest.TrackingCode, src => src.TrackingCode);
// VerifyGoldenPackagePurchase
config.NewConfig<VerifyGoldenPackagePurchaseRequest, VerifyGoldenPackagePurchaseCommand>()
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.Authority, src => src.Authority)
.Map(dest => dest.Status, src => src.Status);
config.NewConfig<VerifyGoldenPackagePurchaseResponseDto, VerifyGoldenPackagePurchaseResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.ReferenceCode, src => src.ReferenceCode)
.Map(dest => dest.WalletBalance, src => src.WalletBalance);
// GetUserPackageStatus
config.NewConfig<GetUserPackageStatusRequest, GetUserPackageStatusQuery>()
.Map(dest => dest.UserId, src => src.UserId);
@@ -55,34 +24,6 @@ public class PackageProfile : IRegister
.Map(dest => dest.LastPurchaseDate, src => src.LastPurchaseDate.HasValue
? Timestamp.FromDateTime(src.LastPurchaseDate.Value.ToUniversalTime())
: null);
// InitiateBasePackagePayment (56M پکیج پایه)
config.NewConfig<InitiateBasePackagePaymentRequest, InitiateBasePackagePaymentCommand>()
.Map(dest => dest.UserId, src => src.UserId);
config.NewConfig<InitiateBasePackagePaymentResponseDto, InitiateBasePackagePaymentResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.Amount, src => src.Amount);
// VerifyBasePackagePayment (تأیید پرداخت پکیج پایه)
config.NewConfig<VerifyBasePackagePaymentRequest, VerifyBasePackagePaymentCommand>()
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.PaymentSuccess, src => src.PaymentSuccess)
.Map(dest => dest.RefId, src => src.RefId)
.Map(dest => dest.Message, src => src.Message);
config.NewConfig<VerifyBasePackagePaymentResponseDto, VerifyBasePackagePaymentResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.ReferenceCode, src => src.ReferenceCode)
.Map(dest => dest.WalletBalance, src => src.WalletBalance)
.Map(dest => dest.DiscountBalance, src => src.DiscountBalance);
}
}
@@ -1,6 +1,6 @@
namespace CMSMicroservice.WebApi.Common.Mappings;
public class UserWalletChangeLogProfile : IRegister
public class UserWalletHistoryProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
@@ -1,249 +0,0 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
using CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge;
using CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Controllers;
/// <summary>
/// Callback endpoint for payment gateways (ZarinPal, etc.)
/// درگاه پرداخت بعد از پرداخت (یا لغو) کاربر را به اینجا redirect می‌کند
/// </summary>
[ApiController]
[AllowAnonymous] // کاربر از درگاه بانک برمی‌گردد — JWT ندارد
[ApiExplorerSettings(GroupName = "cms")]
public class PaymentCallbackController : ControllerBase
{
private readonly ISender _sender;
private readonly IPaymentGatewayService _paymentGateway;
private readonly IApplicationDbContext _context;
private readonly IConfiguration _configuration;
private readonly ILogger<PaymentCallbackController> _logger;
public PaymentCallbackController(
ISender sender,
IPaymentGatewayService paymentGateway,
IApplicationDbContext context,
IConfiguration configuration,
ILogger<PaymentCallbackController> logger)
{
_sender = sender;
_paymentGateway = paymentGateway;
_context = context;
_configuration = configuration;
_logger = logger;
}
/// <summary>
/// Callback برای پرداخت سفارش فروشگاه تخفیفی
/// زرین‌پال کاربر را با Authority و Status به این endpoint برمی‌گرداند
/// </summary>
[HttpGet("/api/payment/discount-order/callback")]
public async Task<IActionResult> DiscountOrderCallback(
[FromQuery] long orderId,
[FromQuery(Name = "Authority")] string? authority,
[FromQuery(Name = "Status")] string? status,
CancellationToken cancellationToken)
{
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
_logger.LogInformation(
"Payment callback received: OrderId={OrderId}, Authority={Authority}, Status={Status}",
orderId, authority, status);
try
{
// پیدا کردن سفارش و تراکنش
var order = await _context.DiscountOrders
.Include(o => o.OrderDetails)
.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
if (order == null)
{
_logger.LogError("Payment callback: Order #{OrderId} not found", orderId);
return Redirect($"{frontOfficeBaseUrl}/discount-store/orders?error=order-not-found");
}
var transaction = order.TransactionId.HasValue
? await _context.Transactions.FirstOrDefaultAsync(
t => t.Id == order.TransactionId.Value, cancellationToken)
: null;
// تأیید پرداخت از درگاه
bool paymentSuccess = false;
string? refId = null;
if (string.Equals(status, "OK", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(authority))
{
// Verify با مبلغ از دیتابیس (ریال)
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
authority,
status!,
order.GatewayAmountPaid, // مبلغ به ریال
cancellationToken);
paymentSuccess = verifyResult.IsSuccess;
refId = verifyResult.TrackingCode ?? verifyResult.RefId;
// آپدیت PaymentTransaction با نتیجه verify
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == authority, cancellationToken);
if (paymentTx != null)
{
paymentTx.PaymentStatus = verifyResult.IsSuccess;
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
paymentTx.VerificationStatusMessage = verifyResult.Message;
paymentTx.CardPan = verifyResult.CardPan;
paymentTx.CardHash = verifyResult.CardHash;
paymentTx.RefId = verifyResult.TrackingCode;
await _context.SaveChangesAsync(cancellationToken);
}
_logger.LogInformation(
"Payment verification for Order #{OrderId}: Success={Success}, RefId={RefId}, Message={Message}",
orderId, paymentSuccess, refId, verifyResult.Message);
}
else
{
_logger.LogWarning("Payment cancelled by user for Order #{OrderId}", orderId);
}
// تکمیل سفارش از طریق CQRS
var completeResult = await _sender.Send(new CompleteOrderPaymentCommand
{
OrderId = orderId,
TransactionId = transaction?.Id ?? 0,
PaymentSuccess = paymentSuccess,
RefId = refId
}, cancellationToken);
// Redirect به FrontOffice
if (paymentSuccess && completeResult.Success)
{
_logger.LogInformation("Payment completed successfully for Order #{OrderId}", orderId);
return Redirect(
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=success");
}
else
{
_logger.LogWarning("Payment failed for Order #{OrderId}", orderId);
return Redirect(
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=failed");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment callback error for Order #{OrderId}", orderId);
return Redirect(
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=error");
}
}
/// <summary>
/// Callback برای شارژ کیف‌پول جادویی — زرین‌پال بعد از پرداخت کاربر را اینجا برمی‌گرداند
/// </summary>
[HttpGet("/api/wallet/verify-magic-charge")]
public async Task<IActionResult> 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");
}
}
/// <summary>
/// Callback برای شارژ کیف‌پول تخفیفی — زرین‌پال بعد از پرداخت کاربر را اینجا برمی‌گرداند
/// </summary>
[HttpGet("/api/wallet/verify-discount-charge")]
public async Task<IActionResult> DiscountChargeCallback(
[FromQuery(Name = "Authority")] string? authority,
[FromQuery(Name = "Status")] string? status,
CancellationToken cancellationToken)
{
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
_logger.LogInformation(
"Discount charge callback received: Authority={Authority}, Status={Status}",
authority, status);
try
{
if (string.IsNullOrEmpty(authority))
{
_logger.LogError("Discount charge callback: Authority is missing");
return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=error&reason=no-authority");
}
// پیدا کردن PaymentTransaction برای استخراج UserId و Amount
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == authority, cancellationToken);
if (paymentTx == null || !paymentTx.UserId.HasValue)
{
_logger.LogError("Discount charge callback: PaymentTransaction not found for Authority={Authority}", authority);
return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=error&reason=tx-not-found");
}
if (!string.Equals(status, "OK", StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("Discount charge cancelled by user. Authority={Authority}", authority);
return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=cancelled");
}
var result = await _sender.Send(new VerifyDiscountWalletChargeCommand
{
UserId = paymentTx.UserId.Value,
Amount = paymentTx.Amount,
Authority = authority
}, cancellationToken);
_logger.LogInformation(
"Discount charge completed successfully. Authority={Authority}, UserId={UserId}",
authority, paymentTx.UserId.Value);
return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=success");
}
catch (Exception ex)
{
_logger.LogError(ex, "Discount charge callback error. Authority={Authority}", authority);
return Redirect($"{frontOfficeBaseUrl}/profile/charge-discount-wallet?payment=failed");
}
}
}
+1 -1
View File
@@ -281,7 +281,7 @@ static bool IsCustomerService(string serviceName)
"User", "UserAddress", "Commission", "NetworkMembership",
"ClubMembership", "UserOrder", "UserWallet", "UserCarts",
"DiscountShoppingCart", "City", "Package", "Transactions",
"UserContract", "UserWalletChangeLog"
"UserContract", "UserWalletHistory"
};
return customerServices.Any(customer => serviceName.Contains(customer, StringComparison.OrdinalIgnoreCase)) &&
@@ -98,8 +98,11 @@ public class CategoryService : CategoryContract.CategoryContractBase
return response;
}
#region [ARCHIVED] GetCategoryByIdForCustomer تکراری: GetCategory (admin) + GetAllCategoriesForCustomer کافی است
[Obsolete("ARCHIVED: Use GetCategory or GetAllCategoriesForCustomer instead")]
public override async Task<GetCategoryByIdForCustomerResponse> GetCategoryByIdForCustomer(GetCategoryByIdForCustomerRequest request, ServerCallContext context)
{
// [ARCHIVED] duplicate — GetCategory + GetAllCategoriesForCustomer cover this
var query = new GetCategoryQuery { Id = request.Id };
var result = await _sender.Send(query, context.CancellationToken);
@@ -118,4 +121,5 @@ public class CategoryService : CategoryContract.CategoryContractBase
}
};
}
#endregion
}
@@ -177,6 +177,8 @@ public class CityService : CityContract.CityContractBase
};
}
#region [ARCHIVED] UpdateCity & DeleteCity شهرها seed data هستند، ویرایش/حذف دستی باعث خرابی آدرسها میشود
[Obsolete("ARCHIVED: Cities are seed data — manual edit/delete breaks user addresses")]
public override async Task<Empty> UpdateCity(
UpdateCityRequest request, ServerCallContext context)
{
@@ -195,6 +197,7 @@ public class CityService : CityContract.CityContractBase
return new Empty();
}
[Obsolete("ARCHIVED: Cities are seed data — manual delete breaks user addresses")]
public override async Task<Empty> DeleteCity(
DeleteCityRequest request, ServerCallContext context)
{
@@ -206,6 +209,7 @@ public class CityService : CityContract.CityContractBase
await _context.SaveChangesAsync(context.CancellationToken);
return new Empty();
}
#endregion
#endregion
}
@@ -2,37 +2,42 @@ using CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
using CMSMicroservice.Application.Common.Authorization;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Protobuf.Protos.Configuration;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Services;
/// <summary>
/// سرویس تنظیمات سیستم - خواندن از SystemConstants
/// سرویس تنظیمات سیستم — مقادیر پکیجی از Package entity خوانده می‌شوند
/// </summary>
public class ConfigurationService : ConfigurationContract.ConfigurationContractBase
{
private readonly ILogger<ConfigurationService> _logger;
private readonly ICurrentUserService _currentUserService;
private readonly IMediator _mediator;
private readonly IApplicationDbContext _context;
public ConfigurationService(
ILogger<ConfigurationService> logger,
ICurrentUserService currentUserService,
IMediator mediator)
IMediator mediator,
IApplicationDbContext context)
{
_logger = logger;
_currentUserService = currentUserService;
_mediator = mediator;
_context = context;
}
/// <summary>
/// دریافت تنظیمات با کلید خاص
/// </summary>
public override Task<GetConfigurationByKeyResponse> GetConfigurationByKey(GetConfigurationByKeyRequest request, ServerCallContext context)
public override async Task<GetConfigurationByKeyResponse> GetConfigurationByKey(GetConfigurationByKeyRequest request, ServerCallContext context)
{
var response = new GetConfigurationByKeyResponse
{
@@ -43,30 +48,33 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
LastModified = Timestamp.FromDateTime(DateTime.UtcNow)
};
// خواندن مقدار از SystemConstants بر اساس کلید
response.Value = GetConfigurationValue(request.Key);
// برای کلیدهای پکیجی، از دیتابیس می‌خوانیم
var package = await GetBasePackageAsync(context.CancellationToken);
response.Value = GetConfigurationValue(request.Key, package);
response.Description = GetConfigurationDescription(request.Key);
_logger.LogDebug("Configuration requested: Key={Key}, Value={Value}", request.Key, response.Value);
return Task.FromResult(response);
return response;
}
/// <summary>
/// دریافت تنظیمات باشگاه مشتریان
/// </summary>
public override Task<GetClubConfigurationResponse> GetClubConfiguration(Empty request, ServerCallContext context)
public override async Task<GetClubConfigurationResponse> GetClubConfiguration(Empty request, ServerCallContext context)
{
var package = await GetBasePackageAsync(context.CancellationToken);
var response = new GetClubConfigurationResponse
{
ActivationFee = SystemConstants.ClubActivationFee,
MembershipGiftValue = SystemConstants.ClubMembershipGiftValue
ActivationFee = package.ActivationFee,
MembershipGiftValue = package.ActivationFee
};
_logger.LogDebug("Club configuration requested: ActivationFee={ActivationFee}, GiftValue={GiftValue}",
response.ActivationFee, response.MembershipGiftValue);
return Task.FromResult(response);
return response;
}
/// <summary>
@@ -111,18 +119,19 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
/// دریافت تمام تنظیمات
/// </summary>
[RequiresPermission(PermissionNames.SettingsView)]
public override Task<GetAllConfigurationsResponse> GetAllConfigurations(GetAllConfigurationsRequest request, ServerCallContext context)
public override async Task<GetAllConfigurationsResponse> GetAllConfigurations(GetAllConfigurationsRequest request, ServerCallContext context)
{
var package = await GetBasePackageAsync(context.CancellationToken);
var response = new GetAllConfigurationsResponse();
// Club Settings
response.Models.Add(CreateConfigModel("Club.ActivationFee", SystemConstants.ClubActivationFee.ToString(), "هزینه فعال‌سازی عضویت باشگاه", 2));
response.Models.Add(CreateConfigModel("Club.MembershipGiftValue", SystemConstants.ClubMembershipGiftValue.ToString(), "مبلغ هدیه حق عضویت باشگاه", 2));
// Club Settings (از Package)
response.Models.Add(CreateConfigModel("Club.ActivationFee", package.ActivationFee.ToString(), "هزینه فعال‌سازی عضویت باشگاه", 2));
response.Models.Add(CreateConfigModel("Club.MembershipGiftValue", package.ActivationFee.ToString(), "مبلغ هدیه حق عضویت باشگاه", 2));
// Commission Settings
// Commission Settings (سقف‌ها از Package)
response.Models.Add(CreateConfigModel("Commission.MinWithdrawalAmount", SystemConstants.CommissionMinWithdrawalAmount.ToString(), "حداقل مبلغ برداشت", 3));
response.Models.Add(CreateConfigModel("Commission.MaxWeeklyBalancesPerLeg", SystemConstants.CommissionMaxWeeklyBalancesPerLeg.ToString(), "سقف تعادل هفتگی برای هر دست", 3));
response.Models.Add(CreateConfigModel("Commission.MaxNetworkLevel", SystemConstants.CommissionMaxNetworkLevel.ToString(), "حداکثر عمق شبکه برای محاسبه کمیسیون", 3));
response.Models.Add(CreateConfigModel("Commission.MaxWeeklyBalancesPerLeg", package.MaxBalancesPerLeg.ToString(), "سقف تعادل هفتگی برای هر دست", 3));
response.Models.Add(CreateConfigModel("Commission.MaxNetworkLevel", package.MaxNetworkLevel.ToString(), "حداکثر عمق شبکه برای محاسبه کمیسیون", 3));
response.Models.Add(CreateConfigModel("Commission.CashWithdrawalEnabled", SystemConstants.CommissionCashWithdrawalEnabled.ToString(), "امکان برداشت نقدی", 3));
response.Models.Add(CreateConfigModel("Commission.CalculationStrategy", SystemConstants.CommissionCalculationStrategy, "روش محاسبه کمیسیون", 3));
@@ -130,9 +139,14 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
response.Models.Add(CreateConfigModel("Network.AllowOrphanNodes", SystemConstants.NetworkAllowOrphanNodes.ToString(), "اجازه حذف والدین با فرزند", 1));
response.Models.Add(CreateConfigModel("Network.MaxChildrenPerLeg", SystemConstants.NetworkMaxChildrenPerLeg.ToString(), "حداکثر تعداد فرزند مستقیم در هر پا", 1));
// Package Settings
response.Models.Add(CreateConfigModel("Package.BasePackageAmount", SystemConstants.BasePackageAmount.ToString(), "مبلغ پکیج پایه", 0));
response.Models.Add(CreateConfigModel("Package.DayaLoanAmount", SystemConstants.DayaLoanAmount.ToString(), "مبلغ وام دایا", 0));
// Package Settings (از Package)
response.Models.Add(CreateConfigModel("Package.BasePackageAmount", package.Price.ToString(), "مبلغ پکیج پایه", 0));
response.Models.Add(CreateConfigModel("Package.DayaLoanAmount", package.Price.ToString(), "مبلغ وام دایا", 0));
// Magic Wallet Settings (از Package)
response.Models.Add(CreateConfigModel("MagicWallet.Multiplier", package.MagicWalletMultiplier.ToString(), "ضریب شارژ کیف‌پول جادویی", 0));
response.Models.Add(CreateConfigModel("MagicWallet.MaxDeposit", package.MagicWalletMaxDeposit.ToString(), "سقف واریز هر دور جادویی", 0));
response.Models.Add(CreateConfigModel("MagicWallet.MaxCredit", package.MagicWalletMaxCredit.ToString(), "سقف اعتبار هر دور جادویی", 0));
// System Settings
response.Models.Add(CreateConfigModel("System.MaintenanceMode", SystemConstants.SystemMaintenanceMode.ToString(), "حالت تعمیر و نگهداری", 0));
@@ -141,7 +155,7 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
// Shop Settings
response.Models.Add(CreateConfigModel("Shop.VAT", SystemConstants.ShopVAT.ToString(), "مالیات بر ارزش افزوده", 0));
return Task.FromResult(response);
return response;
}
/// <summary>
@@ -154,36 +168,43 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
throw new RpcException(new Status(StatusCode.FailedPrecondition, "تنظیمات سیستم فقط خواندنی هستند. تغییر از طریق کد انجام می‌شود"));
}
#region [ARCHIVED] DeactivateConfiguration & GetConfigurationHistory SystemConstants ثابت هستند، این RPCها بیمعنی
[RequiresPermission(PermissionNames.SettingsManageConfiguration)]
[Obsolete("ARCHIVED: SystemConstants are read-only — deactivation is meaningless")]
public override Task<Empty> DeactivateConfiguration(DeactivateConfigurationRequest request, ServerCallContext context)
{
_logger.LogWarning("DeactivateConfiguration called but SystemConstants are read-only. Key: {Key}", request.Key);
_logger.LogWarning("[ARCHIVED] DeactivateConfiguration called but SystemConstants are read-only. Key: {Key}", request.Key);
throw new RpcException(new Status(StatusCode.FailedPrecondition, "تنظیمات سیستم فقط خواندنی هستند. غیرفعال‌سازی از طریق کد انجام می‌شود"));
}
[Obsolete("ARCHIVED: SystemConstants have no history — always returns empty")]
public override Task<GetConfigurationHistoryResponse> GetConfigurationHistory(GetConfigurationHistoryRequest request, ServerCallContext context)
{
// چون constant هستند، تاریخچه‌ای وجود نداره
// [ARCHIVED] چون constant هستند، تاریخچه‌ای وجود نداره
return Task.FromResult(new GetConfigurationHistoryResponse());
}
#endregion
#region Private Helpers
private static string GetConfigurationValue(string key)
private static string GetConfigurationValue(string key, Package? package)
{
return key switch
{
"Club.ActivationFee" => SystemConstants.ClubActivationFee.ToString(),
"Club.MembershipGiftValue" => SystemConstants.ClubMembershipGiftValue.ToString(),
"Club.ActivationFee" => (package?.ActivationFee ?? 0).ToString(),
"Club.MembershipGiftValue" => (package?.ActivationFee ?? 0).ToString(),
"Commission.MinWithdrawalAmount" => SystemConstants.CommissionMinWithdrawalAmount.ToString(),
"Commission.MaxWeeklyBalancesPerLeg" => SystemConstants.CommissionMaxWeeklyBalancesPerLeg.ToString(),
"Commission.MaxNetworkLevel" => SystemConstants.CommissionMaxNetworkLevel.ToString(),
"Commission.MaxWeeklyBalancesPerLeg" => (package?.MaxBalancesPerLeg ?? 0).ToString(),
"Commission.MaxNetworkLevel" => (package?.MaxNetworkLevel ?? 0).ToString(),
"Commission.CashWithdrawalEnabled" => SystemConstants.CommissionCashWithdrawalEnabled.ToString(),
"Commission.CalculationStrategy" => SystemConstants.CommissionCalculationStrategy,
"Network.AllowOrphanNodes" => SystemConstants.NetworkAllowOrphanNodes.ToString(),
"Network.MaxChildrenPerLeg" => SystemConstants.NetworkMaxChildrenPerLeg.ToString(),
"Package.BasePackageAmount" => SystemConstants.BasePackageAmount.ToString(),
"Package.DayaLoanAmount" => SystemConstants.DayaLoanAmount.ToString(),
"Package.BasePackageAmount" => (package?.Price ?? 0).ToString(),
"Package.DayaLoanAmount" => (package?.Price ?? 0).ToString(),
"MagicWallet.Multiplier" => (package?.MagicWalletMultiplier ?? 0).ToString(),
"MagicWallet.MaxDeposit" => (package?.MagicWalletMaxDeposit ?? 0).ToString(),
"MagicWallet.MaxCredit" => (package?.MagicWalletMaxCredit ?? 0).ToString(),
"System.MaintenanceMode" => SystemConstants.SystemMaintenanceMode.ToString(),
"System.EnableAuditLog" => SystemConstants.SystemEnableAuditLog.ToString(),
"Shop.VAT" => SystemConstants.ShopVAT.ToString(),
@@ -191,21 +212,31 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
};
}
private async Task<Package> GetBasePackageAsync(CancellationToken cancellationToken)
{
return await _context.Packages
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
?? throw new RpcException(new Status(StatusCode.Internal, "پکیج پایه یافت نشد"));
}
private static string GetConfigurationDescription(string key)
{
return key switch
{
"Club.ActivationFee" => "هزینه فعال‌سازی عضویت باشگاه (ریال)",
"Club.MembershipGiftValue" => "مبلغ هدیه حق عضویت باشگاه (ریال)",
"Commission.MinWithdrawalAmount" => "حداقل مبلغ برداشت (ریال)",
"Club.ActivationFee" => "هزینه فعال‌سازی عضویت باشگاه (تومان)",
"Club.MembershipGiftValue" => "مبلغ هدیه حق عضویت باشگاه (تومان)",
"Commission.MinWithdrawalAmount" => "حداقل مبلغ برداشت (تومان)",
"Commission.MaxWeeklyBalancesPerLeg" => "سقف تعادل هفتگی برای هر دست",
"Commission.MaxNetworkLevel" => "حداکثر عمق شبکه برای محاسبه کمیسیون",
"Commission.CashWithdrawalEnabled" => "امکان برداشت نقدی",
"Commission.CalculationStrategy" => "روش محاسبه کمیسیون",
"Network.AllowOrphanNodes" => "اجازه حذف والدین با فرزند",
"Network.MaxChildrenPerLeg" => "حداکثر تعداد فرزند مستقیم در هر پا",
"Package.BasePackageAmount" => "مبلغ پکیج پایه (ریال)",
"Package.DayaLoanAmount" => "مبلغ وام دایا (ریال)",
"Package.BasePackageAmount" => "مبلغ پکیج پایه (تومان)",
"Package.DayaLoanAmount" => "مبلغ وام دایا (تومان)",
"MagicWallet.Multiplier" => "ضریب شارژ کیف‌پول جادویی",
"MagicWallet.MaxDeposit" => "سقف واریز هر دور جادویی (تومان)",
"MagicWallet.MaxCredit" => "سقف اعتبار هر دور جادویی (تومان)",
"System.MaintenanceMode" => "حالت تعمیر و نگهداری سیستم",
"System.EnableAuditLog" => "فعال‌سازی لاگ تغییرات",
"Shop.VAT" => "مالیات بر ارزش افزوده",
@@ -13,6 +13,8 @@ using Grpc.Core;
using Google.Protobuf.WellKnownTypes;
using Mapster;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Services;
@@ -21,15 +23,24 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
private readonly ICurrentUserService _currentUserService;
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ILogger<DiscountOrderService> _logger;
public DiscountOrderService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
ICurrentUserService currentUserService)
ICurrentUserService currentUserService,
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
ILogger<DiscountOrderService> logger)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_currentUserService = currentUserService;
_context = context;
_paymentGateway = paymentGateway;
_logger = logger;
}
private long GetCurrentUserId()
@@ -174,6 +185,108 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB
return await _dispatchRequestToCQRS.Handle<GetDiscountSalesReportRequest, GetDiscountSalesReportQuery, GetDiscountSalesReportResponse>(request, context);
}
// ============= Customer Verify Discount Order Payment =============
public override async Task<CustomerVerifyDiscountOrderPaymentResponse> CustomerVerifyDiscountOrderPayment(
CustomerVerifyDiscountOrderPaymentRequest request, ServerCallContext context)
{
_logger.LogInformation(
"CustomerVerifyDiscountOrderPayment called: OrderId={OrderId}, Authority={Authority}, Status={Status}",
request.OrderId, request.Authority, request.Status);
try
{
// پیدا کردن سفارش و تراکنش
var order = await _context.DiscountOrders
.Include(o => o.OrderDetails)
.FirstOrDefaultAsync(o => o.Id == request.OrderId, context.CancellationToken);
if (order == null)
{
_logger.LogError("Order #{OrderId} not found", request.OrderId);
return new CustomerVerifyDiscountOrderPaymentResponse
{
Success = false,
Message = "سفارش یافت نشد",
OrderId = request.OrderId
};
}
var transaction = order.TransactionId.HasValue
? await _context.Transactions.FirstOrDefaultAsync(
t => t.Id == order.TransactionId.Value, context.CancellationToken)
: null;
// تأیید پرداخت از درگاه
bool paymentSuccess = false;
string? refId = null;
if (string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(request.Authority))
{
// Verify با مبلغ از دیتابیس (تومان — تبدیل به ریال در ZarinPalService)
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority,
request.Status,
order.GatewayAmountPaid,
context.CancellationToken);
paymentSuccess = verifyResult.IsSuccess;
refId = verifyResult.TrackingCode ?? verifyResult.RefId;
// آپدیت PaymentTransaction با نتیجه verify
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, context.CancellationToken);
if (paymentTx != null)
{
paymentTx.PaymentStatus = verifyResult.IsSuccess;
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
paymentTx.VerificationStatusMessage = verifyResult.Message;
paymentTx.CardPan = verifyResult.CardPan;
paymentTx.CardHash = verifyResult.CardHash;
paymentTx.RefId = verifyResult.TrackingCode;
await _context.SaveChangesAsync(context.CancellationToken);
}
_logger.LogInformation(
"Payment verification for Order #{OrderId}: Success={Success}, RefId={RefId}",
request.OrderId, paymentSuccess, refId);
}
else
{
_logger.LogWarning("Payment cancelled by user for Order #{OrderId}", request.OrderId);
}
// تکمیل سفارش از طریق CQRS
var completeResult = await _sender.Send(new CompleteOrderPaymentCommand
{
OrderId = request.OrderId,
TransactionId = transaction?.Id ?? 0,
PaymentSuccess = paymentSuccess,
RefId = refId
}, context.CancellationToken);
return new CustomerVerifyDiscountOrderPaymentResponse
{
Success = paymentSuccess && completeResult.Success,
Message = paymentSuccess && completeResult.Success
? "پرداخت سفارش با موفقیت انجام شد"
: "پرداخت سفارش ناموفق بود",
OrderId = request.OrderId
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment verify error for Order #{OrderId}", request.OrderId);
return new CustomerVerifyDiscountOrderPaymentResponse
{
Success = false,
Message = $"خطا در بررسی پرداخت: {ex.Message}",
OrderId = request.OrderId
};
}
}
private static CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus MapPaymentStatus(DomainEnums.PaymentStatus status) => status switch
{
DomainEnums.PaymentStatus.Success => CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus.PaymentCompleted,
@@ -25,9 +25,11 @@ public class HealthService : HealthContract.HealthContractBase
return result.Adapt<GetSystemHealthResponse>();
}
#region [ARCHIVED] GetServiceHealth تکراری: GetSystemHealth کل سیستم را برمیگرداند، فیلتر client-side کافی است
[Obsolete("ARCHIVED: Use GetSystemHealth + client-side filter by service name")]
public override async Task<GetServiceHealthResponse> GetServiceHealth(GetServiceHealthRequest request, ServerCallContext context)
{
// For now, just return system health filtered by service name
// [ARCHIVED] duplicate — GetSystemHealth returns all, client can filter
var systemHealthQuery = new GetSystemHealthQuery();
var systemHealth = await _mediator.Send(systemHealthQuery, context.CancellationToken);
@@ -45,4 +47,5 @@ public class HealthService : HealthContract.HealthContractBase
CheckedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.UtcNow)
};
}
#endregion
}
@@ -3,10 +3,6 @@ using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.PackageCQ.Commands.CreateNewPackage;
using CMSMicroservice.Application.PackageCQ.Commands.UpdatePackage;
using CMSMicroservice.Application.PackageCQ.Commands.DeletePackage;
using CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
using CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
using CMSMicroservice.Application.PackageCQ.Commands.InitiateBasePackagePayment;
using CMSMicroservice.Application.PackageCQ.Commands.VerifyBasePackagePayment;
using CMSMicroservice.Application.PackageCQ.Queries.GetPackage;
using CMSMicroservice.Application.PackageCQ.Queries.GetAllPackageByFilter;
using CMSMicroservice.Application.PackageCQ.Queries.GetUserPackageStatus;
@@ -72,32 +68,11 @@ public class PackageService : PackageContract.PackageContractBase
return await _dispatchRequestToCQRS.Handle<GetAllPackageByFilterRequest, GetAllPackageByFilterQuery, GetAllPackageByFilterResponse>(request, context);
}
public override async Task<PurchaseGoldenPackageResponse> PurchaseGoldenPackage(PurchaseGoldenPackageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<PurchaseGoldenPackageRequest, PurchaseGoldenPackageCommand, PurchaseGoldenPackageResponse>(request, context);
}
public override async Task<VerifyGoldenPackagePurchaseResponse> VerifyGoldenPackagePurchase(VerifyGoldenPackagePurchaseRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<VerifyGoldenPackagePurchaseRequest, VerifyGoldenPackagePurchaseCommand, VerifyGoldenPackagePurchaseResponse>(request, context);
}
public override async Task<GetUserPackageStatusResponse> GetUserPackageStatus(GetUserPackageStatusRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserPackageStatusRequest, GetUserPackageStatusQuery, GetUserPackageStatusResponse>(request, context);
}
// Base Package Payment (56M)
public override async Task<InitiateBasePackagePaymentResponse> InitiateBasePackagePayment(InitiateBasePackagePaymentRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<InitiateBasePackagePaymentRequest, InitiateBasePackagePaymentCommand, InitiateBasePackagePaymentResponse>(request, context);
}
public override async Task<VerifyBasePackagePaymentResponse> VerifyBasePackagePayment(VerifyBasePackagePaymentRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<VerifyBasePackagePaymentRequest, VerifyBasePackagePaymentCommand, VerifyBasePackagePaymentResponse>(request, context);
}
// ============= Customer-specific Method Implementations =============
public override async Task<GetCustomerPackagesResponse> GetCustomerPackages(GetCustomerPackagesRequest request, ServerCallContext context)
@@ -207,13 +182,17 @@ public class PackageService : PackageContract.PackageContractBase
.Select(u => new { u.Mobile })
.FirstOrDefaultAsync(context.CancellationToken);
// Callback URL از config — نه از ورودی کاربر (امنیت)
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
var callbackUrl = $"{frontOfficeBaseUrl}/profile/payment-callback?orderId={purchase.Id}";
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
Amount = package.Price,
UserId = userId,
Mobile = user?.Mobile ?? string.Empty,
Description = $"خرید پکیج {package.Title}",
CallbackUrl = request.CallbackUrl
CallbackUrl = callbackUrl
}, context.CancellationToken);
if (!paymentResult.IsSuccess)
@@ -237,7 +216,7 @@ public class PackageService : PackageContract.PackageContractBase
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
Amount = package.Price,
CallbackUrl = request.CallbackUrl,
CallbackUrl = callbackUrl,
Description = $"خرید پکیج {package.Title}",
Mobile = user?.Mobile,
UserId = userId,
@@ -295,13 +274,15 @@ public class PackageService : PackageContract.PackageContractBase
};
}
// Verify with payment gateway
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority, request.Status, context.CancellationToken);
// آپدیت PaymentTransaction
// واکشی PaymentTransaction برای گرفتن مبلغ (تومان) جهت verify
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, context.CancellationToken);
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
// Verify with payment gateway (مبلغ به تومان — سرویس زرین‌پال خودش ×۱۰ می‌کنه)
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority, request.Status, (decimal)amountInToman, context.CancellationToken);
if (paymentTx != null)
{
paymentTx.PaymentStatus = verifyResult.IsSuccess;
@@ -317,6 +298,51 @@ public class PackageService : PackageContract.PackageContractBase
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Success;
transaction.PaymentDate = DateTime.UtcNow;
transaction.RefId = verifyResult.RefId;
// شارژ کیف پول کاربر
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == purchase.UserId, context.CancellationToken);
if (wallet == null)
{
wallet = new CMSMicroservice.Domain.Entities.UserWallet
{
UserId = purchase.UserId,
Balance = 0,
DiscountBalance = 0,
NetworkBalance = 0
};
_context.UserWallets.Add(wallet);
await _context.SaveChangesAsync(context.CancellationToken);
}
var discountMultiplier = purchase.Package?.DiscountMultiplier
?? throw new RpcException(new Status(StatusCode.Internal, "پکیج سفارش یافت نشد"));
var discountAmount = (long)(purchase.Amount * (double)discountMultiplier);
wallet.Balance += purchase.Amount;
wallet.DiscountBalance += discountAmount;
// ثبت لاگ کیف پول
var walletLog = new CMSMicroservice.Domain.Entities.UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = purchase.Amount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = discountAmount,
IsIncrease = true,
RefrenceId = transaction.Id,
PackageId = purchase.PackageId
};
_context.UserWalletHistories.Add(walletLog);
// به‌روزرسانی کاربر
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Id == purchase.UserId, context.CancellationToken);
if (user != null)
user.PackagePurchaseMethod = Domain.Enums.PackagePurchaseMethod.DirectPurchase;
}
else if (transaction != null)
{
@@ -325,6 +351,24 @@ public class PackageService : PackageContract.PackageContractBase
await _context.SaveChangesAsync(context.CancellationToken);
// فعالسازی خودکار باشگاه مشتریان بعد از تأیید پرداخت موفق
if (verifyResult.IsSuccess)
{
try
{
await _sender.Send(new CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership.ActivateClubMembershipCommand
{
UserId = purchase.UserId,
ForceActivation = false
}, context.CancellationToken);
}
catch (Exception ex)
{
// لاگ خطا ولی پرداخت موفق بوده — کاربر می‌تونه دستی فعالسازی کنه
System.Console.WriteLine($"Auto club activation failed for UserId {purchase.UserId}: {ex.Message}");
}
}
return new CustomerVerifyPackagePurchaseResponse
{
Success = verifyResult.IsSuccess,
@@ -162,6 +162,10 @@ public class TransactionsService : TransactionsContract.TransactionsContractBase
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(context.CancellationToken);
// Callback URL از config — نه از ورودی کاربر (امنیت)
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
var callbackUrl = $"{frontOfficeBaseUrl}/profile/payment-callback";
// Initiate payment with gateway
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
@@ -169,7 +173,7 @@ public class TransactionsService : TransactionsContract.TransactionsContractBase
UserId = userId,
Mobile = request.Mobile ?? user?.Mobile ?? string.Empty,
Description = request.Description ?? "پرداخت آنلاین",
CallbackUrl = request.CallbackUrl
CallbackUrl = callbackUrl
}, context.CancellationToken);
if (!paymentResult.IsSuccess)
@@ -191,7 +195,7 @@ public class TransactionsService : TransactionsContract.TransactionsContractBase
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
Amount = request.Amount,
CallbackUrl = request.CallbackUrl,
CallbackUrl = callbackUrl,
Description = request.Description ?? "پرداخت آنلاین",
Mobile = request.Mobile ?? user?.Mobile,
UserId = userId,
@@ -235,13 +239,15 @@ public class TransactionsService : TransactionsContract.TransactionsContractBase
};
}
// Verify with gateway
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority, request.Status, context.CancellationToken);
// آپدیت PaymentTransaction
// واکشی PaymentTransaction برای گرفتن مبلغ (تومان) جهت verify
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, context.CancellationToken);
var amountInToman = paymentTx?.Amount ?? transaction.Amount;
// Verify with gateway (مبلغ به تومان — سرویس زرین‌پال خودش ×۱۰ می‌کنه)
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority, request.Status, (decimal)amountInToman, context.CancellationToken);
if (paymentTx != null)
{
paymentTx.PaymentStatus = verifyResult.IsSuccess;
@@ -322,7 +322,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
wallet.Balance -= totalAmount;
// Create wallet change log
var walletLog = new UserWalletChangeLog
var walletLog = new UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
@@ -335,20 +335,31 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
RefrenceId = transaction.Id
};
_context.UserWalletChangeLogs.Add(walletLog);
_context.UserWalletHistories.Add(walletLog);
// ═══ Magic Wallet: Entry / Exit Trigger ═══
if (wallet.Balance == 0)
// Q24: آستانه <= 1,000,000 ریال به جای == 0 (چون قیمت محصولات متفاوت است)
if (wallet.Balance <= SystemConstants.MagicWalletEntryThreshold)
{
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)
if (wallet.WalletMode == WalletMode.Magic)
{
// بارگذاری پکیج کاربر برای سقف کیف‌پول جادویی
var userCycle = await _context.ClubMembershipCycles
.FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, context.CancellationToken);
var userPackage = userCycle != null
? await _context.Packages.FirstOrDefaultAsync(p => p.Id == userCycle.PackageId, context.CancellationToken)
: await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, context.CancellationToken);
var magicMaxDeposit = userPackage?.MagicWalletMaxDeposit
?? throw new InvalidOperationException($"Package not found for magic wallet exit check, UserId={userId}");
if (wallet.MagicTotalDeposited >= magicMaxDeposit)
{
// ── EXIT Magic Mode ──
// هر دو شرط: Balance=0 و سقف 100M پر شده
// هر دو شرط: Balance<=Threshold و سقف MagicMaxDeposit پر شده
wallet.WalletMode = WalletMode.Normal;
wallet.MagicCompletedAt = DateTime.UtcNow;
@@ -358,6 +369,14 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
if (currentCycle != null)
{
currentCycle.MagicCompletedAt = DateTime.UtcNow;
currentCycle.IsCurrentCycle = false;
}
// B6 fix: ریست PackagePurchaseMethod تا Guards G1-G3 خرید مجدد را مسدود نکنند
if (user != null)
{
user.PackagePurchaseMethod = PackagePurchaseMethod.None;
}
}
}
else if (wallet.WalletMode == WalletMode.Normal
@@ -380,7 +399,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
currentCycle.MagicStartedAt = DateTime.UtcNow;
}
}
// ⚠️ اگه Magic باشه و Balance=0 ولی سقف پر نشده → هنوز Magic!
// ⚠️ اگه Magic باشه و Balance<=Threshold ولی سقف پر نشده → هنوز Magic!
// کاربر میتونه دوباره شارژ کنه
}
@@ -497,7 +516,9 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
};
}
#region [ARCHIVED] GetOrdersByDateRange تکراری: GetAllUserOrderByFilter همین قابلیت + فیلترهای بیشتر دارد
[RequiresPermission(PermissionNames.ReportsView)]
[Obsolete("ARCHIVED: Use GetAllUserOrderByFilter instead — has date range + more filters")]
public override async Task<GetOrdersByDateRangeResponse> GetOrdersByDateRange(GetOrdersByDateRangeRequest request, ServerCallContext context)
{
var pageNumber = request.PageNumber > 0 ? request.PageNumber : 1;
@@ -551,6 +572,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
Orders = { orders }
};
}
#endregion
[RequiresPermission(PermissionNames.OrdersUpdate)]
public override async Task<ApplyDiscountToOrderResponse> ApplyDiscountToOrder(ApplyDiscountToOrderRequest request, ServerCallContext context)
@@ -582,7 +604,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
};
}
[RequiresPermission(PermissionNames.OrdersView)]
// [RequiresPermission(PermissionNames.OrdersView)]
public override async Task<CalculateOrderPVResponse> CalculateOrderPV(CalculateOrderPVRequest request, ServerCallContext context)
{
var order = await _context.UserOrders
@@ -624,17 +646,21 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
// ============= Customer-specific Methods =============
#region [ARCHIVED] CreateNewOrderForCustomer & SubmitOrderForCustomer Stub خالی: مسیر سفارش مشتری از SubmitShopBuyOrder میگذرد
[Obsolete("ARCHIVED: Empty stub — customer order flow uses SubmitShopBuyOrder")]
public override async Task<CreateNewUserOrderResponse> CreateNewOrderForCustomer(CreateNewUserOrderRequest request, ServerCallContext context)
{
// For now, return empty response - will be implemented properly later
// [ARCHIVED] Empty stub — مسیر سفارش مشتری از SubmitShopBuyOrder می‌گذرد
return new CreateNewUserOrderResponse();
}
[Obsolete("ARCHIVED: Empty stub — customer order flow uses SubmitShopBuyOrder")]
public override async Task<SubmitShopBuyOrderResponse> SubmitOrderForCustomer(SubmitShopBuyOrderRequest request, ServerCallContext context)
{
// For now, return empty response - will be implemented properly later
// [ARCHIVED] Empty stub — مسیر سفارش مشتری از SubmitShopBuyOrder می‌گذرد
return new SubmitShopBuyOrderResponse();
}
#endregion
public override async Task<GetAllUserOrderByFilterResponse> GetCustomerOrders(GetAllUserOrderByFilterRequest request, ServerCallContext context)
{
@@ -807,7 +833,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
if (wallet != null)
{
wallet.Balance += refundAmount;
_context.UserWalletChangeLogs.Add(new UserWalletChangeLog
_context.UserWalletHistories.Add(new UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
@@ -35,6 +35,7 @@ public class UserService : UserContract.UserContractBase
private readonly ICurrentUserService _currentUserService;
private readonly IHashService _hashService;
private readonly CMSMicroservice.Application.Common.FileManager.IFileManager _fileManager;
private readonly IGenerateJwtToken _generateJwt;
public UserService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
@@ -42,7 +43,8 @@ public class UserService : UserContract.UserContractBase
IApplicationDbContext context,
ICurrentUserService currentUserService,
IHashService hashService,
CMSMicroservice.Application.Common.FileManager.IFileManager fileManager)
CMSMicroservice.Application.Common.FileManager.IFileManager fileManager,
IGenerateJwtToken generateJwt)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
@@ -50,6 +52,7 @@ public class UserService : UserContract.UserContractBase
_currentUserService = currentUserService;
_hashService = hashService;
_fileManager = fileManager;
_generateJwt = generateJwt;
}
public override async Task<CreateNewUserResponse> CreateNewUser(CreateNewUserRequest request, ServerCallContext context)
{
@@ -115,12 +118,20 @@ public class UserService : UserContract.UserContractBase
var user = await _context.Users
.AsNoTracking()
.Include(u => u.UserContracts)
.ThenInclude(uc => uc.Contract)
.Include(u => u.UserRoles)
.ThenInclude(ur => ur.Role)
.Include(u => u.ClubMembership)
.Where(u => u.Id == userId && !u.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (user == null)
throw new RpcException(new Status(StatusCode.NotFound, "کاربر یافت نشد"));
// تولید توکن JWT با آخرین اطلاعات کاربر
var token = await _generateJwt.GenerateJwtToken(user);
return new GetUserForCustomerResponse
{
Id = user.Id,
@@ -141,7 +152,8 @@ public class UserService : UserContract.UserContractBase
PushNotifications = user.PushNotifications,
BirthDate = user.BirthDate.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(user.BirthDate.Value, DateTimeKind.Utc))
: null
: null,
Token = token
};
}
@@ -1,37 +0,0 @@
using CMSMicroservice.Protobuf.Protos.UserWalletChangeLog;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.CreateNewUserWalletChangeLog;
using CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.UpdateUserWalletChangeLog;
using CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.DeleteUserWalletChangeLog;
using CMSMicroservice.Application.UserWalletChangeLogCQ.Queries.GetUserWalletChangeLog;
using CMSMicroservice.Application.UserWalletChangeLogCQ.Queries.GetAllUserWalletChangeLogByFilter;
namespace CMSMicroservice.WebApi.Services;
public class UserWalletChangeLogService : UserWalletChangeLogContract.UserWalletChangeLogContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public UserWalletChangeLogService(IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<CreateNewUserWalletChangeLogResponse> CreateNewUserWalletChangeLog(CreateNewUserWalletChangeLogRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateNewUserWalletChangeLogRequest, CreateNewUserWalletChangeLogCommand, CreateNewUserWalletChangeLogResponse>(request, context);
}
public override async Task<Empty> UpdateUserWalletChangeLog(UpdateUserWalletChangeLogRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateUserWalletChangeLogRequest, UpdateUserWalletChangeLogCommand>(request, context);
}
public override async Task<Empty> DeleteUserWalletChangeLog(DeleteUserWalletChangeLogRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteUserWalletChangeLogRequest, DeleteUserWalletChangeLogCommand>(request, context);
}
public override async Task<GetUserWalletChangeLogResponse> GetUserWalletChangeLog(GetUserWalletChangeLogRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserWalletChangeLogRequest, GetUserWalletChangeLogQuery, GetUserWalletChangeLogResponse>(request, context);
}
public override async Task<GetAllUserWalletChangeLogByFilterResponse> GetAllUserWalletChangeLogByFilter(GetAllUserWalletChangeLogByFilterRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllUserWalletChangeLogByFilterRequest, GetAllUserWalletChangeLogByFilterQuery, GetAllUserWalletChangeLogByFilterResponse>(request, context);
}
}
@@ -0,0 +1,37 @@
using CMSMicroservice.Protobuf.Protos.UserWalletHistory;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.UserWalletHistoryCQ.Commands.CreateNewUserWalletHistory;
using CMSMicroservice.Application.UserWalletHistoryCQ.Commands.UpdateUserWalletHistory;
using CMSMicroservice.Application.UserWalletHistoryCQ.Commands.DeleteUserWalletHistory;
using CMSMicroservice.Application.UserWalletHistoryCQ.Queries.GetUserWalletHistory;
using CMSMicroservice.Application.UserWalletHistoryCQ.Queries.GetAllUserWalletHistoryByFilter;
namespace CMSMicroservice.WebApi.Services;
public class UserWalletHistoryService : UserWalletHistoryContract.UserWalletHistoryContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public UserWalletHistoryService(IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<CreateNewUserWalletHistoryResponse> CreateNewUserWalletHistory(CreateNewUserWalletHistoryRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateNewUserWalletHistoryRequest, CreateNewUserWalletHistoryCommand, CreateNewUserWalletHistoryResponse>(request, context);
}
public override async Task<Empty> UpdateUserWalletHistory(UpdateUserWalletHistoryRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateUserWalletHistoryRequest, UpdateUserWalletHistoryCommand>(request, context);
}
public override async Task<Empty> DeleteUserWalletHistory(DeleteUserWalletHistoryRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteUserWalletHistoryRequest, DeleteUserWalletHistoryCommand>(request, context);
}
public override async Task<GetUserWalletHistoryResponse> GetUserWalletHistory(GetUserWalletHistoryRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserWalletHistoryRequest, GetUserWalletHistoryQuery, GetUserWalletHistoryResponse>(request, context);
}
public override async Task<GetAllUserWalletHistoryByFilterResponse> GetAllUserWalletHistoryByFilter(GetAllUserWalletHistoryByFilterRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllUserWalletHistoryByFilterRequest, GetAllUserWalletHistoryByFilterQuery, GetAllUserWalletHistoryByFilterResponse>(request, context);
}
}
@@ -5,9 +5,11 @@ using CMSMicroservice.Application.UserWalletCQ.Commands.UpdateUserWallet;
using CMSMicroservice.Application.UserWalletCQ.Commands.DeleteUserWallet;
using CMSMicroservice.Application.WalletCQ.Commands.ChargeMagicWallet;
using CMSMicroservice.Application.WalletCQ.Commands.ChargeDiscountWallet;
using CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge;
using CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetUserWallet;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetAllUserWalletByFilter;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletChangeLog;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawals;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawalSettings;
using CMSMicroservice.Application.Common.Interfaces;
@@ -16,6 +18,7 @@ using CMSMicroservice.Domain.Enums;
using Grpc.Core;
using Google.Protobuf.WellKnownTypes;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
@@ -25,17 +28,20 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
private readonly ILogger<UserWalletService> _logger;
public UserWalletService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
IApplicationDbContext context,
ICurrentUserService currentUserService)
ICurrentUserService currentUserService,
ILogger<UserWalletService> logger)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
_currentUserService = currentUserService;
_logger = logger;
}
public override async Task<CreateNewUserWalletResponse> CreateNewUserWallet(CreateNewUserWalletRequest request, ServerCallContext context)
{
@@ -102,9 +108,9 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
};
}
public override async Task<GetCustomerWalletChangeLogResponse> GetCustomerWalletChangeLog(GetCustomerWalletChangeLogRequest request, ServerCallContext context)
public override async Task<GetCustomerWalletHistoryResponse> GetCustomerWalletHistory(GetCustomerWalletHistoryRequest request, ServerCallContext context)
{
var query = new GetCustomerWalletChangeLogQuery
var query = new GetCustomerWalletHistoryQuery
{
ReferenceId = request.ReferenceId,
IsIncrease = request.IsIncrease
@@ -112,7 +118,7 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
var changeLogs = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerWalletChangeLogResponse
var response = new GetCustomerWalletHistoryResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
@@ -127,7 +133,7 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
foreach (var log in changeLogs)
{
response.Models.Add(new CustomerWalletChangeLogModel
response.Models.Add(new CustomerWalletHistoryModel
{
CurrentBalance = log.CurrentBalance,
ChangeValue = log.ChangeValue,
@@ -213,6 +219,94 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
};
}
// ============= Wallet Verify Methods =============
public override async Task<VerifyWalletChargeResponse> VerifyMagicCharge(
VerifyWalletChargeRequest request, ServerCallContext context)
{
_logger.LogInformation("VerifyMagicCharge called: Authority={Authority}, Status={Status}",
request.Authority, request.Status);
try
{
if (string.IsNullOrEmpty(request.Authority))
return new VerifyWalletChargeResponse { Success = false, Message = "کد Authority نامعتبر است" };
var result = await _sender.Send(new VerifyMagicWalletChargeCommand
{
Authority = request.Authority,
Status = request.Status ?? "NOK"
}, context.CancellationToken);
_logger.LogInformation("VerifyMagicCharge result: {Result}, Authority={Authority}", result, request.Authority);
return new VerifyWalletChargeResponse
{
Success = result,
Message = result
? "شارژ کیف‌پول جادویی با موفقیت انجام شد"
: "شارژ کیف‌پول جادویی ناموفق بود"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "VerifyMagicCharge error: Authority={Authority}", request.Authority);
return new VerifyWalletChargeResponse { Success = false, Message = ex.Message };
}
}
public override async Task<VerifyWalletChargeResponse> VerifyDiscountCharge(
VerifyWalletChargeRequest request, ServerCallContext context)
{
_logger.LogInformation("VerifyDiscountCharge called: Authority={Authority}, Status={Status}",
request.Authority, request.Status);
try
{
if (string.IsNullOrEmpty(request.Authority))
return new VerifyWalletChargeResponse { Success = false, Message = "کد Authority نامعتبر است" };
// پیدا کردن PaymentTransaction برای استخراج UserId و Amount
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, context.CancellationToken);
if (paymentTx == null || !paymentTx.UserId.HasValue)
{
_logger.LogError("VerifyDiscountCharge: PaymentTransaction not found for Authority={Authority}", request.Authority);
return new VerifyWalletChargeResponse { Success = false, Message = "تراکنش یافت نشد" };
}
if (!string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("VerifyDiscountCharge: Payment cancelled by user. Authority={Authority}", request.Authority);
return new VerifyWalletChargeResponse { Success = false, Message = "پرداخت توسط کاربر لغو شد" };
}
var result = await _sender.Send(new VerifyDiscountWalletChargeCommand
{
UserId = paymentTx.UserId.Value,
Amount = paymentTx.Amount,
Authority = request.Authority
}, context.CancellationToken);
_logger.LogInformation("VerifyDiscountCharge result: {Result}, Authority={Authority}, UserId={UserId}",
result, request.Authority, paymentTx.UserId.Value);
return new VerifyWalletChargeResponse
{
Success = result,
Message = result
? "شارژ کیف پول تخفیفی با موفقیت انجام شد"
: "شارژ کیف پول تخفیفی ناموفق بود"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "VerifyDiscountCharge error: Authority={Authority}", request.Authority);
return new VerifyWalletChargeResponse { Success = false, Message = ex.Message };
}
}
public override async Task<GetMagicWalletStatusResponse> GetMagicWalletStatus(
Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context)
{
@@ -227,15 +321,27 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
var cycleCount = await _context.ClubMembershipCycles
.CountAsync(c => c.UserId == userId, context.CancellationToken);
// بارگذاری پکیج کاربر برای سقف کیف‌پول جادویی
var currentCycle = await _context.ClubMembershipCycles
.FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, context.CancellationToken);
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 ?? SystemConstants.MagicWalletDefaultMaxDeposit;
var magicMultiplier = package != null ? (double)package.MagicWalletMultiplier : SystemConstants.MagicWalletDefaultMultiplier;
var magicMaxCredit = package?.MagicWalletMaxCredit ?? SystemConstants.MagicWalletDefaultMaxCredit;
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),
MagicMaxDeposit = magicMaxDeposit,
MagicRemainingDeposit = Math.Max(0, magicMaxDeposit - wallet.MagicTotalDeposited),
Balance = wallet.Balance,
PurchaseCycleCount = cycleCount
PurchaseCycleCount = cycleCount,
MagicMultiplier = magicMultiplier,
MagicMaxCredit = magicMaxCredit
};
if (wallet.MagicActivatedAt.HasValue)