feat: Add ClearCart command and response, implement CancelOrder command with validation, and enhance DeliveryStatus and User models

This commit is contained in:
masoodafar-web
2025-12-02 03:30:36 +03:30
parent 25fc73ae28
commit 78606cc5cc
100 changed files with 12925 additions and 8137 deletions

View File

@@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2" />
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />

View File

@@ -1,22 +1,15 @@
using CMSMicroservice.Application.Common.Models;
using MediatR;
namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
/// <summary>
/// Command برای فعال‌سازی عضویت باشگاه مشتریان یک کاربر
/// </summary>
public record ActivateClubMembershipCommand : IRequest<long>
public record ActivateClubMembershipCommand : IRequest<bool>
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; init; }
/// <summary>
/// تاریخ فعال‌سازی (اختیاری - پیش‌فرض: الان)
/// </summary>
public DateTimeOffset? ActivationDate { get; init; }
/// <summary>
/// دلیل فعال‌سازی (برای History)
/// </summary>
public string? Reason { get; init; }
}

View File

@@ -1,98 +1,220 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Entities.Club;
using CMSMicroservice.Domain.Entities.History;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClubMembershipCommand, long>
public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClubMembershipCommand, bool>
{
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser;
private readonly ILogger<ActivateClubMembershipCommandHandler> _logger;
public ActivateClubMembershipCommandHandler(
IApplicationDbContext context,
ICurrentUserService currentUser)
ICurrentUserService currentUser,
ILogger<ActivateClubMembershipCommandHandler> logger)
{
_context = context;
_currentUser = currentUser;
_logger = logger;
}
public async Task<long> Handle(ActivateClubMembershipCommand request, CancellationToken cancellationToken)
public async Task<bool> Handle(
ActivateClubMembershipCommand request,
CancellationToken cancellationToken)
{
// بررسی وجود کاربر
var userExists = await _context.Users
.AnyAsync(x => x.Id == request.UserId, cancellationToken);
if (!userExists)
try
{
throw new NotFoundException(nameof(User), request.UserId);
}
_logger.LogInformation(
"Activating club membership for UserId: {UserId}",
request.UserId
);
// دریافت مبلغ عضویت از Configuration
var activationFeeConfig = await _context.SystemConfigurations
.Where(x => x.Key == "Club.ActivationFee" && x.IsActive)
.Select(x => x.Value)
.FirstOrDefaultAsync(cancellationToken);
long initialContribution = long.Parse(activationFeeConfig ?? "25000000"); // Default: 25 million Rials
// 1. بررسی کاربر
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
// بررسی عضویت فعلی
var existingMembership = await _context.ClubMemberships
.FirstOrDefaultAsync(x => x.UserId == request.UserId, cancellationToken);
ClubMembership entity;
bool isNewMembership = existingMembership == null;
var activationDate = request.ActivationDate ?? DateTimeOffset.Now; // استفاده از Local Time
if (isNewMembership)
{
// ایجاد عضویت جدید
// توجه: InitialContribution فقط ثبت می‌شود، از کیف پول کسر نمی‌شود!
// کاربر قبلاً باید کیف پول خود را شارژ کرده باشد
entity = new ClubMembership
if (user == null)
{
UserId = request.UserId,
IsActive = true,
ActivatedAt = activationDate.DateTime,
InitialContribution = initialContribution, // مبلغ عضویت از Configuration
TotalEarned = 0
};
_logger.LogWarning("User not found: {UserId}", request.UserId);
throw new NotFoundException(nameof(User), request.UserId);
}
await _context.ClubMemberships.AddAsync(entity, cancellationToken);
}
else
{
// فعال‌سازی مجدد عضویت موجود
entity = existingMembership;
if (entity.IsActive)
// 2. بررسی اینکه پکیج خریده باشد
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
{
// اگر از قبل فعال است، فقط تاریخ را به‌روز می‌کنیم
entity.ActivatedAt = activationDate.DateTime;
_logger.LogWarning(
"User {UserId} has not purchased golden package yet",
request.UserId
);
throw new BadRequestException(
"برای فعالسازی باشگاه مشتریان ابتدا باید پکیج طلایی خریداری کنید"
);
}
// 3. بررسی موجودی کیف پول
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == user.Id, cancellationToken);
if (wallet == null)
{
_logger.LogError("Wallet not found for UserId: {UserId}", request.UserId);
throw new NotFoundException("کیف پول کاربر یافت نشد");
}
if (wallet.Balance < 56_000_000)
{
_logger.LogWarning(
"User {UserId} has insufficient balance: {Balance}",
request.UserId,
wallet.Balance
);
throw new BadRequestException(
"برای فعالسازی باشگاه مشتریان باید حداقل 56 میلیون تومان موجودی اصلی داشته باشید"
);
}
// 4. پیدا کردن UserOrder با PackageId
var packageOrder = await _context.UserOrders
.Include(o => o.Transaction)
.Where(o =>
o.UserId == user.Id &&
o.PackageId != null &&
o.PaymentStatus == PaymentStatus.Success)
.OrderByDescending(o => o.Created)
.FirstOrDefaultAsync(cancellationToken);
if (packageOrder == null)
{
_logger.LogWarning(
"No successful package order found for UserId: {UserId}",
request.UserId
);
throw new NotFoundException("سفارش پکیج طلایی یافت نشد");
}
// 5. بررسی Transaction
if (packageOrder.Transaction == null)
{
_logger.LogError(
"Transaction not found for OrderId: {OrderId}",
packageOrder.Id
);
throw new NotFoundException("تراکنش مربوط به سفارش یافت نشد");
}
var transaction = packageOrder.Transaction;
if (transaction.Type != TransactionType.DepositIpg &&
transaction.Type != TransactionType.DepositExternal1)
{
_logger.LogWarning(
"Invalid transaction type for OrderId {OrderId}: {Type}",
packageOrder.Id,
transaction.Type
);
throw new BadRequestException(
"تراکنش معتبر برای فعالسازی باشگاه یافت نشد"
);
}
// 6. بررسی عضویت فعلی
var existingMembership = await _context.ClubMemberships
.FirstOrDefaultAsync(c => c.UserId == user.Id, cancellationToken);
ClubMembership entity;
bool isNewMembership = existingMembership == null;
var activationDate = DateTime.UtcNow;
if (isNewMembership)
{
// ایجاد عضویت جدید
entity = new ClubMembership
{
UserId = user.Id,
IsActive = true,
ActivatedAt = activationDate,
InitialContribution = 56_000_000,
TotalEarned = 0,
PurchaseMethod = user.PackagePurchaseMethod
};
_context.ClubMemberships.Add(entity);
_logger.LogInformation(
"Created new club membership for UserId {UserId} via {Method}",
user.Id,
user.PackagePurchaseMethod
);
}
else
{
// فعال‌سازی عضویت غیرفعال
if (existingMembership.IsActive)
{
_logger.LogInformation(
"User {UserId} is already an active club member",
user.Id
);
return true;
}
// فعال‌سازی مجدد
entity = existingMembership;
entity.IsActive = true;
entity.ActivatedAt = activationDate.DateTime;
entity.ActivatedAt = activationDate;
entity.PurchaseMethod = user.PackagePurchaseMethod;
_context.ClubMemberships.Update(entity);
_logger.LogInformation(
"Reactivated club membership for UserId {UserId}",
user.Id
);
}
_context.ClubMemberships.Update(entity);
await _context.SaveChangesAsync(cancellationToken);
// 7. ثبت تاریخچه
var history = new ClubMembershipHistory
{
ClubMembershipId = entity.Id,
UserId = entity.UserId,
OldIsActive = !isNewMembership && !existingMembership!.IsActive,
NewIsActive = true,
Action = ClubMembershipAction.Activated,
Reason = isNewMembership
? $"Initial activation via {user.PackagePurchaseMethod}"
: $"Reactivated via {user.PackagePurchaseMethod}",
PerformedBy = _currentUser.GetPerformedBy()
};
_context.ClubMembershipHistories.Add(history);
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Club membership activated successfully. UserId: {UserId}, MembershipId: {MembershipId}",
user.Id,
entity.Id
);
return true;
}
await _context.SaveChangesAsync(cancellationToken);
// ثبت تاریخچه
var history = new ClubMembershipHistory
catch (Exception ex)
{
ClubMembershipId = entity.Id,
UserId = entity.UserId,
OldIsActive = !isNewMembership && !existingMembership!.IsActive,
NewIsActive = true,
Action = ClubMembershipAction.Activated,
Reason = request.Reason ?? (isNewMembership ? "Initial activation" : "Reactivated"),
PerformedBy = _currentUser.GetPerformedBy()
};
await _context.ClubMembershipHistories.AddAsync(history, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return entity.Id;
_logger.LogError(
ex,
"Error in ActivateClubMembershipCommand for UserId: {UserId}",
request.UserId
);
throw;
}
}
}

View File

@@ -7,16 +7,6 @@ public class ActivateClubMembershipCommandValidator : AbstractValidator<Activate
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر معتبر نیست");
RuleFor(x => x.ActivationDate)
.LessThanOrEqualTo(DateTimeOffset.UtcNow.AddDays(1))
.WithMessage("تاریخ فعال‌سازی نمی‌تواند در آینده باشد")
.When(x => x.ActivationDate.HasValue);
RuleFor(x => x.Reason)
.MaximumLength(500)
.WithMessage("دلیل فعال‌سازی نمی‌تواند بیشتر از 500 کاراکتر باشد")
.When(x => !string.IsNullOrEmpty(x.Reason));
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>

View File

@@ -1,21 +1,30 @@
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.CommissionCQ.Commands.ProcessWithdrawal;
public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawalCommand, Unit>
{
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ILogger<ProcessWithdrawalCommandHandler> _logger;
public ProcessWithdrawalCommandHandler(
IApplicationDbContext context,
ICurrentUserService currentUser)
ICurrentUserService currentUser,
IPaymentGatewayService paymentGateway,
ILogger<ProcessWithdrawalCommandHandler> logger)
{
_context = context;
_currentUser = currentUser;
_paymentGateway = paymentGateway;
_logger = logger;
}
public async Task<Unit> Handle(ProcessWithdrawalCommand request, CancellationToken cancellationToken)
{
var payout = await _context.UserCommissionPayouts
.Include(x => x.User)
.FirstOrDefaultAsync(x => x.Id == request.PayoutId, cancellationToken);
if (payout == null)
@@ -35,12 +44,9 @@ public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawal
if (request.IsApproved)
{
// تایید برداشت
payout.Status = CommissionPayoutStatus.Withdrawn;
payout.WithdrawnAt = now;
// اگر روش برداشت Diamond بود، باید مبلغ به کیف پول تخفیف اضافه شود
if (payout.WithdrawalMethod == WithdrawalMethod.Diamond)
{
// روش Diamond: شارژ کیف پول تخفیف
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(x => x.UserId == payout.UserId, cancellationToken);
@@ -49,6 +55,61 @@ public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawal
wallet.DiscountBalance += payout.TotalAmount;
_context.UserWallets.Update(wallet);
}
payout.Status = CommissionPayoutStatus.Withdrawn;
payout.WithdrawnAt = now;
}
else if (payout.WithdrawalMethod == WithdrawalMethod.Cash)
{
// روش انتقال بانکی: فراخوانی Payment Gateway
try
{
_logger.LogInformation("Processing bank transfer for Payout {PayoutId}, User {UserId}, Amount {Amount}",
payout.Id, payout.UserId, payout.TotalAmount);
var payoutRequest = new PayoutRequest
{
Amount = payout.TotalAmount,
UserId = payout.UserId,
Iban = payout.IbanNumber ?? throw new InvalidOperationException("شماره شبا یافت نشد"),
AccountHolderName = $"{payout.User.FirstName} {payout.User.LastName}",
Description = $"برداشت کمیسیون هفته {payout.WeekNumber}",
InternalRefId = $"PAYOUT-{payout.Id}"
};
var payoutResult = await _paymentGateway.ProcessPayoutAsync(payoutRequest, cancellationToken);
if (payoutResult.IsSuccess)
{
payout.Status = CommissionPayoutStatus.Withdrawn;
payout.WithdrawnAt = now;
payout.BankReferenceId = payoutResult.BankRefId;
payout.BankTrackingCode = payoutResult.TrackingCode;
_logger.LogInformation("Bank transfer successful: Payout {PayoutId}, BankRef {BankRef}",
payout.Id, payoutResult.BankRefId);
}
else
{
// خطا در واریز
payout.Status = CommissionPayoutStatus.PaymentFailed;
payout.PaymentFailureReason = payoutResult.Message;
_logger.LogError("Bank transfer failed: Payout {PayoutId}, Reason: {Reason}",
payout.Id, payoutResult.Message);
throw new InvalidOperationException($"خطا در واریز: {payoutResult.Message}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception during bank transfer for Payout {PayoutId}", payout.Id);
payout.Status = CommissionPayoutStatus.PaymentFailed;
payout.PaymentFailureReason = ex.Message;
throw;
}
}
_context.UserCommissionPayouts.Update(payout);
@@ -63,9 +124,9 @@ public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawal
AmountBefore = payout.TotalAmount,
AmountAfter = payout.TotalAmount,
OldStatus = oldStatus,
NewStatus = CommissionPayoutStatus.Withdrawn,
NewStatus = payout.Status,
Action = CommissionPayoutAction.Withdrawn,
PerformedBy = "Admin", // TODO: باید از Current User گرفته شود
PerformedBy = _currentUser.UserId ?? "Admin",
Reason = $"تایید برداشت به روش {payout.WithdrawalMethod}"
};
@@ -92,7 +153,7 @@ public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawal
OldStatus = oldStatus,
NewStatus = CommissionPayoutStatus.Paid,
Action = CommissionPayoutAction.Cancelled,
PerformedBy = "Admin", // TODO: باید از Current User گرفته شود
PerformedBy = _currentUser.UserId ?? "Admin",
Reason = request.Reason ?? "درخواست برداشت رد شد"
};

View File

@@ -35,5 +35,6 @@ public interface IApplicationDbContext
DbSet<UserCommissionPayout> UserCommissionPayouts { get; }
DbSet<CommissionPayoutHistory> CommissionPayoutHistories { get; }
DbSet<WorkerExecutionLog> WorkerExecutionLogs { get; }
DbSet<DayaLoanContract> DayaLoanContracts { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,194 @@
namespace CMSMicroservice.Application.Common.Interfaces;
/// <summary>
/// Interface برای یکپارچه‌سازی با درگاه‌های پرداخت
/// </summary>
public interface IPaymentGatewayService
{
/// <summary>
/// شروع تراکنش پرداخت (ارسال به درگاه)
/// </summary>
/// <param name="request">اطلاعات تراکنش</param>
/// <param name="cancellationToken"></param>
/// <returns>URL درگاه برای هدایت کاربر + RefId تراکنش</returns>
Task<PaymentInitiateResult> InitiatePaymentAsync(
PaymentRequest request,
CancellationToken cancellationToken = default);
/// <summary>
/// تأیید پرداخت (بعد از بازگشت از درگاه)
/// </summary>
/// <param name="refId">شماره مرجع تراکنش</param>
/// <param name="verificationToken">توکن تأیید از درگاه</param>
/// <param name="cancellationToken"></param>
/// <returns>وضعیت نهایی تراکنش</returns>
Task<PaymentVerificationResult> VerifyPaymentAsync(
string refId,
string verificationToken,
CancellationToken cancellationToken = default);
/// <summary>
/// واریز مبلغ به حساب کاربر (برداشت از کیف پول)
/// </summary>
/// <param name="request">اطلاعات واریز</param>
/// <param name="cancellationToken"></param>
/// <returns>وضعیت واریز</returns>
Task<PayoutResult> ProcessPayoutAsync(
PayoutRequest request,
CancellationToken cancellationToken = default);
}
/// <summary>
/// درخواست شروع تراکنش پرداخت
/// </summary>
public class PaymentRequest
{
/// <summary>
/// مبلغ (تومان)
/// </summary>
public decimal Amount { get; set; }
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; set; }
/// <summary>
/// شماره موبایل
/// </summary>
public string Mobile { get; set; } = string.Empty;
/// <summary>
/// شرح تراکنش
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// URL بازگشت بعد از پرداخت
/// </summary>
public string CallbackUrl { get; set; } = string.Empty;
}
/// <summary>
/// نتیجه شروع تراکنش
/// </summary>
public class PaymentInitiateResult
{
/// <summary>
/// موفق بودن درخواست
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// شماره مرجع تراکنش (RefId)
/// </summary>
public string? RefId { get; set; }
/// <summary>
/// URL درگاه برای هدایت کاربر
/// </summary>
public string? GatewayUrl { get; set; }
/// <summary>
/// پیام خطا (در صورت ناموفق بودن)
/// </summary>
public string? ErrorMessage { get; set; }
}
/// <summary>
/// نتیجه تأیید تراکنش
/// </summary>
public class PaymentVerificationResult
{
/// <summary>
/// موفق بودن تراکنش
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// شماره مرجع تراکنش
/// </summary>
public string RefId { get; set; } = string.Empty;
/// <summary>
/// کد پیگیری بانک
/// </summary>
public string? TrackingCode { get; set; }
/// <summary>
/// مبلغ تراکنش
/// </summary>
public decimal Amount { get; set; }
/// <summary>
/// پیام
/// </summary>
public string? Message { get; set; }
}
/// <summary>
/// درخواست واریز
/// </summary>
public class PayoutRequest
{
/// <summary>
/// مبلغ (تومان)
/// </summary>
public decimal Amount { get; set; }
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; set; }
/// <summary>
/// شماره شبا
/// </summary>
public string Iban { get; set; } = string.Empty;
/// <summary>
/// نام صاحب حساب
/// </summary>
public string AccountHolderName { get; set; } = string.Empty;
/// <summary>
/// شرح واریز
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// شماره مرجع داخلی
/// </summary>
public string InternalRefId { get; set; } = string.Empty;
}
/// <summary>
/// نتیجه واریز
/// </summary>
public class PayoutResult
{
/// <summary>
/// موفق بودن واریز
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// شماره مرجع تراکنش بانکی
/// </summary>
public string? BankRefId { get; set; }
/// <summary>
/// کد پیگیری
/// </summary>
public string? TrackingCode { get; set; }
/// <summary>
/// پیام
/// </summary>
public string? Message { get; set; }
/// <summary>
/// زمان پردازش
/// </summary>
public DateTime ProcessedAt { get; set; }
}

View File

@@ -4,7 +4,8 @@ public class TransactionsProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
//config.NewConfig<Source,Destination>()
// .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}");
// VerifyTransactionCommand → domain mapping handled in handler
// RefundTransactionCommand → domain mapping handled in handler
}
}

View File

@@ -0,0 +1,14 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckDayaLoanStatus;
/// <summary>
/// Command برای استعلام وضعیت وام از سرویس دایا
/// </summary>
public record CheckDayaLoanStatusCommand : IRequest<CheckDayaLoanStatusResponseDto>
{
/// <summary>
/// لیست کدهای ملی برای استعلام
/// </summary>
public List<string> NationalCodes { get; init; }
}

View File

@@ -0,0 +1,108 @@
using CMSMicroservice.Domain.Events;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Application.DayaLoanCQ.Services;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckDayaLoanStatus;
public class CheckDayaLoanStatusCommandHandler : IRequestHandler<CheckDayaLoanStatusCommand, CheckDayaLoanStatusResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IDayaLoanApiService _dayaApiService;
private readonly ILogger<CheckDayaLoanStatusCommandHandler> _logger;
public CheckDayaLoanStatusCommandHandler(
IApplicationDbContext context,
IDayaLoanApiService dayaApiService,
ILogger<CheckDayaLoanStatusCommandHandler> logger)
{
_context = context;
_dayaApiService = dayaApiService;
_logger = logger;
}
public async Task<CheckDayaLoanStatusResponseDto> Handle(CheckDayaLoanStatusCommand request, CancellationToken cancellationToken)
{
var results = new List<DayaLoanStatusItem>();
try
{
// فراخوانی سرویس دایا (Mock یا Real)
var dayaResults = await _dayaApiService.CheckLoanStatusAsync(request.NationalCodes, cancellationToken);
foreach (var dayaResult in dayaResults)
{
try
{
results.Add(new DayaLoanStatusItem
{
NationalCode = dayaResult.NationalCode,
Status = dayaResult.Status,
ContractNumber = dayaResult.ContractNumber,
Message = "استعلام موفق"
});
// ذخیره یا به‌روزرسانی در دیتابیس
var existingContract = await _context.DayaLoanContracts
.FirstOrDefaultAsync(d => d.NationalCode == dayaResult.NationalCode, cancellationToken);
if (existingContract != null)
{
existingContract.LastCheckDate = DateTime.UtcNow;
existingContract.Status = dayaResult.Status;
existingContract.ContractNumber = dayaResult.ContractNumber;
}
else
{
var user = await _context.Users
.FirstOrDefaultAsync(u => u.NationalCode == dayaResult.NationalCode, cancellationToken);
if (user != null)
{
var newContract = new DayaLoanContract
{
UserId = user.Id,
NationalCode = dayaResult.NationalCode,
Status = dayaResult.Status,
ContractNumber = dayaResult.ContractNumber,
LastCheckDate = DateTime.UtcNow,
IsProcessed = false
};
await _context.DayaLoanContracts.AddAsync(newContract, cancellationToken);
}
}
await _context.SaveChangesAsync(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing Daya result for {NationalCode}", dayaResult.NationalCode);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calling Daya API service");
// در صورت خطا، نتایج خالی برمی‌گردانیم
foreach (var nationalCode in request.NationalCodes)
{
results.Add(new DayaLoanStatusItem
{
NationalCode = nationalCode,
Status = DayaLoanStatus.PendingReceive,
ContractNumber = null,
Message = $"خطا در استعلام: {ex.Message}"
});
}
}
return new CheckDayaLoanStatusResponseDto
{
Results = results,
TotalChecked = request.NationalCodes.Count,
SuccessCount = results.Count(r => r.ContractNumber != null)
};
}
}

View File

@@ -0,0 +1,18 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckDayaLoanStatus;
public class CheckDayaLoanStatusResponseDto
{
public List<DayaLoanStatusItem> Results { get; set; }
public int TotalChecked { get; set; }
public int SuccessCount { get; set; }
}
public class DayaLoanStatusItem
{
public string NationalCode { get; set; }
public DayaLoanStatus Status { get; set; }
public string? ContractNumber { get; set; }
public string Message { get; set; }
}

View File

@@ -0,0 +1,34 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
/// <summary>
/// Command برای پردازش تایید وام دایا و شارژ کیف پول
/// </summary>
public record ProcessDayaLoanApprovalCommand : IRequest<ProcessDayaLoanApprovalResponseDto>
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; init; }
/// <summary>
/// شماره قرارداد دایا
/// </summary>
public string ContractNumber { get; init; }
/// <summary>
/// مبلغ کیف پول عادی (56 میلیون)
/// </summary>
public long WalletAmount { get; init; } = 56_000_000;
/// <summary>
/// مبلغ کیف پول قفل شده (56 میلیون)
/// </summary>
public long LockedWalletAmount { get; init; } = 56_000_000;
/// <summary>
/// مبلغ کیف پول تخفیف (56 میلیون)
/// </summary>
public long DiscountWalletAmount { get; init; } = 56_000_000;
}

View File

@@ -0,0 +1,169 @@
using CMSMicroservice.Domain.Events;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
public class ProcessDayaLoanApprovalCommandHandler : IRequestHandler<ProcessDayaLoanApprovalCommand, ProcessDayaLoanApprovalResponseDto>
{
private readonly IApplicationDbContext _context;
public ProcessDayaLoanApprovalCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<ProcessDayaLoanApprovalResponseDto> Handle(ProcessDayaLoanApprovalCommand request, CancellationToken cancellationToken)
{
// پیدا کردن کاربر
var user = await _context.Users
.Include(u => u.UserWallets)
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
if (user == null)
{
throw new NotFoundException(nameof(User), request.UserId);
}
// چک کردن که قبلاً دریافت نکرده باشد
if (user.HasReceivedDayaCredit)
{
throw new InvalidOperationException($"کاربر {request.UserId} قبلاً اعتبار دایا را دریافت کرده است");
}
// ایجاد تراکنش با RefId = شماره قرارداد دایا
var transaction = new Transactions
{
Amount = request.WalletAmount + request.LockedWalletAmount + request.DiscountWalletAmount, // 168 میلیون
Description = $"دریافت اعتبار دایا - قرارداد {request.ContractNumber}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow,
RefId = request.ContractNumber, // شماره قرارداد دایا
Type = TransactionType.DepositExternal1
};
await _context.Transactionss.AddAsync(transaction, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
// یافتن یا ایجاد کیف پول کاربر
var wallet = user.UserWallets.FirstOrDefault();
if (wallet == null)
{
wallet = new UserWallet
{
UserId = request.UserId,
Balance = 0,
NetworkBalance = 0,
DiscountBalance = 0
};
await _context.UserWallets.AddAsync(wallet, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
// شارژ کیف پول عادی (56 میلیون)
var balanceBeforeMain = wallet.Balance;
wallet.Balance += request.WalletAmount;
// لاگ کیف پول عادی
var mainLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = request.WalletAmount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(mainLog, cancellationToken);
// شارژ کیف پول شبکه/کارمزد (56 میلیون) - نام‌گذاری قدیم: کیف پول قفل شده
var balanceBeforeLocked = wallet.NetworkBalance;
wallet.NetworkBalance += request.LockedWalletAmount;
// لاگ کیف پول شبکه
var networkLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = 0,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = request.LockedWalletAmount,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(networkLog, cancellationToken);
// شارژ کیف پول تخفیف (56 میلیون)
var balanceBeforeDiscount = wallet.DiscountBalance;
wallet.DiscountBalance += request.DiscountWalletAmount;
// لاگ کیف پول تخفیف
var discountLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = 0,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = request.DiscountWalletAmount,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(discountLog, cancellationToken);
// به‌روزرسانی وضعیت کاربر
user.HasReceivedDayaCredit = true;
user.DayaCreditReceivedAt = DateTime.UtcNow;
// تنظیم نحوه خرید پکیج به DayaLoan
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan;
// ثبت سفارش پکیج طلایی
var goldenPackage = await _context.Packages
.FirstOrDefaultAsync(p => p.Title.Contains("طلایی") || p.Title.Contains("Golden"), cancellationToken);
if (goldenPackage != null)
{
// پیدا کردن آدرس پیش‌فرض کاربر
var defaultAddress = await _context.UserAddresss
.Where(a => a.UserId == request.UserId)
.OrderByDescending(a => a.Created)
.FirstOrDefaultAsync(cancellationToken);
if (defaultAddress != null)
{
var packageOrder = new UserOrder
{
UserId = request.UserId,
PackageId = goldenPackage.Id,
Amount = request.WalletAmount, // 56 میلیون
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow,
DeliveryStatus = DeliveryStatus.None,
UserAddressId = defaultAddress.Id,
TransactionId = transaction.Id,
PaymentMethod = PaymentMethod.IPG
};
await _context.UserOrders.AddAsync(packageOrder, cancellationToken);
}
}
// ثبت Event
user.AddDomainEvent(new DayaLoanApprovedEvent(user, transaction, request.ContractNumber));
await _context.SaveChangesAsync(cancellationToken);
return new ProcessDayaLoanApprovalResponseDto
{
UserId = user.Id,
TransactionId = transaction.Id,
ContractNumber = request.ContractNumber,
MainWalletBalance = wallet.Balance,
LockedWalletBalance = wallet.NetworkBalance,
DiscountWalletBalance = wallet.DiscountBalance,
Message = "اعتبار دایا با موفقیت دریافت شد"
};
}
}

View File

@@ -0,0 +1,21 @@
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
public class ProcessDayaLoanApprovalCommandValidator : AbstractValidator<ProcessDayaLoanApprovalCommand>
{
public ProcessDayaLoanApprovalCommandValidator()
{
RuleFor(v => v.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
RuleFor(v => v.ContractNumber)
.NotEmpty()
.WithMessage("شماره قرارداد الزامی است")
.MaximumLength(100)
.WithMessage("شماره قرارداد نباید بیش از 100 کاراکتر باشد");
RuleFor(v => v.WalletAmount)
.GreaterThan(0)
.WithMessage("مبلغ کیف پول باید بزرگتر از صفر باشد");
}
}

View File

@@ -0,0 +1,12 @@
namespace CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
public class ProcessDayaLoanApprovalResponseDto
{
public long UserId { get; set; }
public long TransactionId { get; set; }
public string ContractNumber { get; set; }
public long MainWalletBalance { get; set; }
public long LockedWalletBalance { get; set; }
public long DiscountWalletBalance { get; set; }
public string Message { get; set; }
}

View File

@@ -0,0 +1,26 @@
using Microsoft.Extensions.Logging;
using CMSMicroservice.Domain.Events;
namespace CMSMicroservice.Application.DayaLoanCQ.EventHandlers.DayaLoanApprovedEventHandlers;
public class DayaLoanApprovedEventHandler : INotificationHandler<DayaLoanApprovedEvent>
{
private readonly ILogger<DayaLoanApprovedEventHandler> _logger;
public DayaLoanApprovedEventHandler(ILogger<DayaLoanApprovedEventHandler> logger)
{
_logger = logger;
}
public Task Handle(DayaLoanApprovedEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Daya loan approved for user {UserId}. Contract: {ContractNumber}, Transaction: {TransactionId}",
notification.User.Id,
notification.ContractNumber,
notification.Transaction.Id);
// اینجا می‌تونیم اعلان به کاربر بفرستیم (Email/SMS)
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,30 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.DayaLoanCQ.Services;
/// <summary>
/// Interface for Daya Loan API Service
/// این سرویس برای ارتباط با API واقعی دایا استفاده می‌شود
/// </summary>
public interface IDayaLoanApiService
{
/// <summary>
/// استعلام وضعیت وام دایا برای یک لیست کدملی
/// </summary>
/// <param name="nationalCodes">لیست کدملی‌های کاربران</param>
/// <param name="cancellationToken"></param>
/// <returns>وضعیت وام به همراه شماره قرارداد (در صورت وجود)</returns>
Task<List<DayaLoanStatusResult>> CheckLoanStatusAsync(
List<string> nationalCodes,
CancellationToken cancellationToken = default);
}
/// <summary>
/// نتیجه استعلام وضعیت وام از سرویس دایا
/// </summary>
public class DayaLoanStatusResult
{
public string NationalCode { get; set; } = string.Empty;
public DayaLoanStatus Status { get; set; }
public string? ContractNumber { get; set; }
}

View File

@@ -0,0 +1,18 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
/// <summary>
/// دستور خرید پکیج طلایی از طریق درگاه بانکی
/// </summary>
public class PurchaseGoldenPackageCommand : IRequest<PaymentInitiateResult>
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; set; }
}

View File

@@ -0,0 +1,160 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using ValidationException = FluentValidation.ValidationException;
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
public class PurchaseGoldenPackageCommandHandler
: IRequestHandler<PurchaseGoldenPackageCommand, PaymentInitiateResult>
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ILogger<PurchaseGoldenPackageCommandHandler> _logger;
public PurchaseGoldenPackageCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
ILogger<PurchaseGoldenPackageCommandHandler> logger)
{
_context = context;
_paymentGateway = paymentGateway;
_logger = logger;
}
public async Task<PaymentInitiateResult> Handle(
PurchaseGoldenPackageCommand request,
CancellationToken cancellationToken)
{
try
{
_logger.LogInformation(
"Starting golden package purchase for UserId: {UserId}",
request.UserId
);
// 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. بررسی اینکه قبلاً پکیج نخریده باشد
if (user.PackagePurchaseMethod != PackagePurchaseMethod.None)
{
_logger.LogWarning(
"User {UserId} has already purchased package via {Method}",
request.UserId,
user.PackagePurchaseMethod
);
throw new ValidationException(
"شما قبلاً پکیج طلایی را خریداری کرده‌اید"
);
}
// 3. پیدا کردن پکیج طلایی
var goldenPackage = await _context.Packages
.FirstOrDefaultAsync(
p => p.Title.Contains("طلایی") || p.Title.Contains("Golden"),
cancellationToken
);
if (goldenPackage == null)
{
_logger.LogError("Golden package not found in database");
throw new NotFoundException("پکیج طلایی یافت نشد");
}
// 4. پیدا کردن آدرس پیش‌فرض کاربر (برای فیلد اجباری)
var defaultAddress = await _context.UserAddresss
.Where(a => a.UserId == request.UserId)
.OrderByDescending(a => a.Created)
.FirstOrDefaultAsync(cancellationToken);
if (defaultAddress == null)
{
_logger.LogWarning("No address found for user {UserId}", request.UserId);
throw new ValidationException(
"لطفاً ابتدا یک آدرس برای خود ثبت کنید"
);
}
// 5. ایجاد سفارش
var order = new UserOrder
{
UserId = user.Id,
PackageId = goldenPackage.Id,
Amount = goldenPackage.Price, // 56,000,000 تومان
PaymentStatus = PaymentStatus.Pending,
DeliveryStatus = DeliveryStatus.None,
UserAddressId = defaultAddress.Id,
PaymentMethod = PaymentMethod.IPG
};
_context.UserOrders.Add(order);
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Created UserOrder {OrderId} for UserId {UserId}, Amount: {Amount}",
order.Id,
request.UserId,
order.Amount
);
// 6. ایجاد درخواست پرداخت از درگاه
var paymentRequest = new PaymentRequest
{
Amount = order.Amount,
UserId = user.Id,
Mobile = user.Mobile ?? "",
CallbackUrl = $"https://yourdomain.com/api/package/verify-golden-package",
Description = $"خرید پکیج طلایی - سفارش #{order.Id}"
};
var paymentResult = await _paymentGateway.InitiatePaymentAsync(paymentRequest);
if (!paymentResult.IsSuccess)
{
_logger.LogError(
"Payment gateway failed for OrderId {OrderId}: {ErrorMessage}",
order.Id,
paymentResult.ErrorMessage
);
// به‌روزرسانی وضعیت سفارش
order.PaymentStatus = PaymentStatus.Reject;
await _context.SaveChangesAsync(cancellationToken);
throw new Exception(
$"خطا در ارتباط با درگاه پرداخت: {paymentResult.ErrorMessage}"
);
}
_logger.LogInformation(
"Payment initiated successfully. OrderId: {OrderId}, RefId: {RefId}",
order.Id,
paymentResult.RefId
);
return paymentResult;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in PurchaseGoldenPackageCommand for UserId: {UserId}",
request.UserId
);
throw;
}
}
}

View File

@@ -0,0 +1,13 @@
using FluentValidation;
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
public class PurchaseGoldenPackageCommandValidator : AbstractValidator<PurchaseGoldenPackageCommand>
{
public PurchaseGoldenPackageCommandValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
}
}

View File

@@ -0,0 +1,21 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using MediatR;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
/// <summary>
/// دستور تأیید پرداخت پکیج طلایی و شارژ کیف پول
/// </summary>
public class VerifyGoldenPackagePurchaseCommand : IRequest<bool>
{
/// <summary>
/// شناسه سفارش
/// </summary>
public long OrderId { get; set; }
/// <summary>
/// کد Authority از درگاه
/// </summary>
public string Authority { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,189 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using ValidationException = FluentValidation.ValidationException;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
public class VerifyGoldenPackagePurchaseCommandHandler
: IRequestHandler<VerifyGoldenPackagePurchaseCommand, bool>
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ILogger<VerifyGoldenPackagePurchaseCommandHandler> _logger;
public VerifyGoldenPackagePurchaseCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
ILogger<VerifyGoldenPackagePurchaseCommandHandler> logger)
{
_context = context;
_paymentGateway = paymentGateway;
_logger = logger;
}
public async Task<bool> Handle(
VerifyGoldenPackagePurchaseCommand request,
CancellationToken cancellationToken)
{
try
{
_logger.LogInformation(
"Verifying golden package purchase. OrderId: {OrderId}, Authority: {Authority}",
request.OrderId,
request.Authority
);
// 1. پیدا کردن سفارش
var order = await _context.UserOrders
.Include(o => o.Package)
.Include(o => o.User)
.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
if (order == null)
{
_logger.LogWarning("Order not found: {OrderId}", request.OrderId);
throw new NotFoundException(nameof(UserOrder), request.OrderId);
}
// 2. بررسی اینکه سفارش قبلاً پرداخت نشده باشد
if (order.PaymentStatus == PaymentStatus.Success)
{
_logger.LogWarning("Order {OrderId} is already paid", request.OrderId);
return true;
}
// 3. Verify با درگاه بانکی
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority,
request.Authority // verificationToken - در بعضی درگاه‌ها همان Authority است
);
if (!verifyResult.IsSuccess)
{
_logger.LogWarning(
"Payment verification failed for OrderId {OrderId}: {Message}",
request.OrderId,
verifyResult.Message
);
order.PaymentStatus = PaymentStatus.Reject;
await _context.SaveChangesAsync(cancellationToken);
throw new ValidationException($"تراکنش ناموفق: {verifyResult.Message}");
}
// 4. شارژ کیف پول کاربر
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == order.UserId, cancellationToken);
if (wallet == null)
{
_logger.LogError("Wallet not found for UserId: {UserId}", order.UserId);
throw new NotFoundException($"کیف پول کاربر با شناسه {order.UserId} یافت نشد");
}
// شارژ Balance (موجودی عادی)
var oldBalance = wallet.Balance;
wallet.Balance += order.Amount;
_logger.LogInformation(
"Charging Balance for UserId {UserId}: {OldBalance} -> {NewBalance}",
order.UserId,
oldBalance,
wallet.Balance
);
// شارژ DiscountBalance (موجودی تخفیف)
var oldDiscountBalance = wallet.DiscountBalance;
wallet.DiscountBalance += order.Amount;
_logger.LogInformation(
"Charging DiscountBalance for UserId {UserId}: {OldBalance} -> {NewBalance}",
order.UserId,
oldDiscountBalance,
wallet.DiscountBalance
);
// 5. ثبت Transaction
var transaction = new Transactions
{
Amount = order.Amount,
Description = $"خرید پکیج طلایی از درگاه - سفارش #{order.Id}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow,
RefId = verifyResult.RefId,
Type = TransactionType.DepositIpg
};
_context.Transactionss.Add(transaction);
await _context.SaveChangesAsync(cancellationToken);
// 6. ثبت لاگ تغییر Balance
var balanceLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = order.Amount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance - order.Amount, // قبل از شارژ DiscountBalance
ChangeDiscountValue = 0,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(balanceLog, cancellationToken);
// 7. ثبت لاگ تغییر DiscountBalance
var discountLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = 0,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = order.Amount,
IsIncrease = true,
RefrenceId = transaction.Id
};
await _context.UserWalletChangeLogs.AddAsync(discountLog, cancellationToken);
// 8. به‌روزرسانی Order
order.TransactionId = transaction.Id;
order.PaymentStatus = PaymentStatus.Success;
order.PaymentDate = DateTime.UtcNow;
order.PaymentMethod = PaymentMethod.IPG;
// 9. تغییر User.PackagePurchaseMethod
order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Golden package purchase verified successfully. " +
"OrderId: {OrderId}, UserId: {UserId}, TransactionId: {TransactionId}, RefId: {RefId}",
order.Id,
order.UserId,
transaction.Id,
verifyResult.RefId
);
return true;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in VerifyGoldenPackagePurchaseCommand. OrderId: {OrderId}",
request.OrderId
);
throw;
}
}
}

View File

@@ -0,0 +1,22 @@
namespace CMSMicroservice.Application.TransactionsCQ.Commands.RefundTransaction;
/// <summary>
/// Command برای استرداد تراکنش
/// </summary>
public record RefundTransactionCommand : IRequest<RefundTransactionResponseDto>
{
/// <summary>
/// شناسه تراکنش برای استرداد
/// </summary>
public long TransactionId { get; init; }
/// <summary>
/// دلیل استرداد
/// </summary>
public string RefundReason { get; init; }
/// <summary>
/// مبلغ استرداد (اگر null باشد، کل مبلغ استرداد می‌شود)
/// </summary>
public long? RefundAmount { get; init; }
}

View File

@@ -0,0 +1,64 @@
using CMSMicroservice.Domain.Events;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.TransactionsCQ.Commands.RefundTransaction;
public class RefundTransactionCommandHandler : IRequestHandler<RefundTransactionCommand, RefundTransactionResponseDto>
{
private readonly IApplicationDbContext _context;
public RefundTransactionCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<RefundTransactionResponseDto> Handle(RefundTransactionCommand request, CancellationToken cancellationToken)
{
// پیدا کردن تراکنش اصلی
var originalTransaction = await _context.Transactionss
.FirstOrDefaultAsync(t => t.Id == request.TransactionId, cancellationToken);
if (originalTransaction == null)
{
throw new NotFoundException(nameof(Transactions), request.TransactionId);
}
// چک کردن که تراکنش Success باشد
if (originalTransaction.PaymentStatus != PaymentStatus.Success)
{
throw new InvalidOperationException($"فقط تراکنش‌های موفق قابل استرداد هستند. وضعیت فعلی: {originalTransaction.PaymentStatus}");
}
// محاسبه مبلغ استرداد
var refundAmount = request.RefundAmount ?? originalTransaction.Amount;
if (refundAmount > originalTransaction.Amount)
{
throw new InvalidOperationException("مبلغ استرداد نمی‌تواند بیشتر از مبلغ اصلی باشد");
}
// ایجاد تراکنش استرداد جدید
var refundTransaction = new Transactions
{
Amount = -refundAmount, // مبلغ منفی برای استرداد
Description = $"استرداد تراکنش {request.TransactionId}: {request.RefundReason}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow,
RefId = $"REFUND-{originalTransaction.RefId}",
Type = TransactionType.Buy // یا می‌تونیم یک نوع جدید برای Refund تعریف کنیم
};
await _context.Transactionss.AddAsync(refundTransaction, cancellationToken);
refundTransaction.AddDomainEvent(new RefundTransactionEvent(refundTransaction, originalTransaction));
await _context.SaveChangesAsync(cancellationToken);
return new RefundTransactionResponseDto
{
OriginalTransactionId = originalTransaction.Id,
RefundTransactionId = refundTransaction.Id,
RefundAmount = refundAmount,
Message = "استرداد با موفقیت انجام شد"
};
}
}

View File

@@ -0,0 +1,22 @@
namespace CMSMicroservice.Application.TransactionsCQ.Commands.RefundTransaction;
public class RefundTransactionCommandValidator : AbstractValidator<RefundTransactionCommand>
{
public RefundTransactionCommandValidator()
{
RuleFor(v => v.TransactionId)
.GreaterThan(0)
.WithMessage("شناسه تراکنش باید بزرگتر از صفر باشد");
RuleFor(v => v.RefundReason)
.NotEmpty()
.WithMessage("دلیل استرداد الزامی است")
.MaximumLength(500)
.WithMessage("دلیل استرداد نباید بیش از 500 کاراکتر باشد");
RuleFor(v => v.RefundAmount)
.GreaterThan(0)
.When(v => v.RefundAmount.HasValue)
.WithMessage("مبلغ استرداد باید بزرگتر از صفر باشد");
}
}

View File

@@ -0,0 +1,9 @@
namespace CMSMicroservice.Application.TransactionsCQ.Commands.RefundTransaction;
public class RefundTransactionResponseDto
{
public long OriginalTransactionId { get; set; }
public long RefundTransactionId { get; set; }
public long RefundAmount { get; set; }
public string Message { get; set; }
}

View File

@@ -0,0 +1,29 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.TransactionsCQ.Commands.VerifyTransaction;
/// <summary>
/// Command برای تایید پرداخت (Callback از درگاه)
/// </summary>
public record VerifyTransactionCommand : IRequest<VerifyTransactionResponseDto>
{
/// <summary>
/// شناسه تراکنش در سیستم
/// </summary>
public long TransactionId { get; init; }
/// <summary>
/// کد رهگیری از درگاه پرداخت (RefId)
/// </summary>
public string RefId { get; init; }
/// <summary>
/// وضعیت پرداخت از درگاه
/// </summary>
public PaymentStatus Status { get; init; }
/// <summary>
/// تاریخ پرداخت
/// </summary>
public DateTime PaymentDate { get; init; }
}

View File

@@ -0,0 +1,52 @@
using CMSMicroservice.Domain.Events;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.TransactionsCQ.Commands.VerifyTransaction;
public class VerifyTransactionCommandHandler : IRequestHandler<VerifyTransactionCommand, VerifyTransactionResponseDto>
{
private readonly IApplicationDbContext _context;
public VerifyTransactionCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<VerifyTransactionResponseDto> Handle(VerifyTransactionCommand request, CancellationToken cancellationToken)
{
// پیدا کردن تراکنش
var transaction = await _context.Transactionss
.FirstOrDefaultAsync(t => t.Id == request.TransactionId, cancellationToken);
if (transaction == null)
{
throw new NotFoundException(nameof(Transactions), request.TransactionId);
}
// چک کردن که تراکنش در وضعیت Pending باشد
if (transaction.PaymentStatus != PaymentStatus.Pending)
{
throw new InvalidOperationException($"Transaction {request.TransactionId} is not in Pending status. Current status: {transaction.PaymentStatus}");
}
// به‌روزرسانی وضعیت تراکنش
transaction.PaymentStatus = request.Status;
transaction.RefId = request.RefId;
transaction.PaymentDate = request.PaymentDate;
// ثبت Event
transaction.AddDomainEvent(new VerifyTransactionEvent(transaction));
await _context.SaveChangesAsync(cancellationToken);
return new VerifyTransactionResponseDto
{
TransactionId = transaction.Id,
Status = transaction.PaymentStatus,
RefId = transaction.RefId,
Message = transaction.PaymentStatus == PaymentStatus.Success
? "پرداخت با موفقیت انجام شد"
: "پرداخت ناموفق بود"
};
}
}

View File

@@ -0,0 +1,21 @@
namespace CMSMicroservice.Application.TransactionsCQ.Commands.VerifyTransaction;
public class VerifyTransactionCommandValidator : AbstractValidator<VerifyTransactionCommand>
{
public VerifyTransactionCommandValidator()
{
RuleFor(v => v.TransactionId)
.GreaterThan(0)
.WithMessage("شناسه تراکنش باید بزرگتر از صفر باشد");
RuleFor(v => v.RefId)
.NotEmpty()
.WithMessage("کد رهگیری الزامی است")
.MaximumLength(100)
.WithMessage("کد رهگیری نباید بیش از 100 کاراکتر باشد");
RuleFor(v => v.PaymentDate)
.NotEmpty()
.WithMessage("تاریخ پرداخت الزامی است");
}
}

View File

@@ -0,0 +1,11 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.TransactionsCQ.Commands.VerifyTransaction;
public class VerifyTransactionResponseDto
{
public long TransactionId { get; set; }
public PaymentStatus Status { get; set; }
public string RefId { get; set; }
public string Message { get; set; }
}

View File

@@ -0,0 +1,26 @@
using Microsoft.Extensions.Logging;
using CMSMicroservice.Domain.Events;
namespace CMSMicroservice.Application.TransactionsCQ.EventHandlers.RefundTransactionEventHandlers;
public class RefundTransactionEventHandler : INotificationHandler<RefundTransactionEvent>
{
private readonly ILogger<RefundTransactionEventHandler> _logger;
public RefundTransactionEventHandler(ILogger<RefundTransactionEventHandler> logger)
{
_logger = logger;
}
public Task Handle(RefundTransactionEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Transaction {OriginalId} refunded with new transaction {RefundId}. Amount: {Amount}",
notification.OriginalTransaction.Id,
notification.RefundTransaction.Id,
notification.RefundTransaction.Amount);
// اینجا می‌تونیم اعلان به کاربر بفرستیم یا کارهای دیگه انجام بدیم
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.Extensions.Logging;
using CMSMicroservice.Domain.Events;
namespace CMSMicroservice.Application.TransactionsCQ.EventHandlers.VerifyTransactionEventHandlers;
public class VerifyTransactionEventHandler : INotificationHandler<VerifyTransactionEvent>
{
private readonly ILogger<VerifyTransactionEventHandler> _logger;
public VerifyTransactionEventHandler(ILogger<VerifyTransactionEventHandler> logger)
{
_logger = logger;
}
public Task Handle(VerifyTransactionEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Transaction {TransactionId} verified with status {Status} and RefId {RefId}",
notification.Item.Id,
notification.Item.PaymentStatus,
notification.Item.RefId);
return Task.CompletedTask;
}
}

View File

@@ -7,6 +7,8 @@ public record CreateNewUserCommand : IRequest<CreateNewUserResponseDto>
public string? LastName { get; init; }
//شماره موبایل
public string Mobile { get; init; }
//ایمیل
public string? Email { get; init; }
//کد ملی
public string? NationalCode { get; init; }
//آدرس آواتار

View File

@@ -7,6 +7,8 @@ public record UpdateUserCommand : IRequest<Unit>
public string? FirstName { get; init; }
//نام خانوادگی
public string? LastName { get; init; }
//ایمیل
public string? Email { get; init; }
//کد ملی
public string? NationalCode { get; init; }
//آدرس آواتار

View File

@@ -0,0 +1,12 @@
namespace CMSMicroservice.Application.UserCartsCQ.Commands.ClearCart;
/// <summary>
/// Command برای پاک کردن تمام سبد خرید کاربر
/// </summary>
public record ClearCartCommand : IRequest<ClearCartResponseDto>
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; init; }
}

View File

@@ -0,0 +1,52 @@
using CMSMicroservice.Domain.Events;
namespace CMSMicroservice.Application.UserCartsCQ.Commands.ClearCart;
public class ClearCartCommandHandler : IRequestHandler<ClearCartCommand, ClearCartResponseDto>
{
private readonly IApplicationDbContext _context;
public ClearCartCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<ClearCartResponseDto> Handle(ClearCartCommand request, CancellationToken cancellationToken)
{
// پیدا کردن تمام آیتم‌های سبد خرید کاربر
var cartItems = await _context.UserCartss
.Where(c => c.UserId == request.UserId)
.ToListAsync(cancellationToken);
if (!cartItems.Any())
{
return new ClearCartResponseDto
{
UserId = request.UserId,
RemovedItemsCount = 0,
Message = "سبد خرید خالی است"
};
}
var itemsCount = cartItems.Count;
// حذف تمام آیتم‌ها
_context.UserCartss.RemoveRange(cartItems);
// ثبت Event
// می‌تونیم یک Event برای هر آیتم یا یک Event کلی بفرستیم
foreach (var item in cartItems)
{
item.AddDomainEvent(new ClearCartEvent(item));
}
await _context.SaveChangesAsync(cancellationToken);
return new ClearCartResponseDto
{
UserId = request.UserId,
RemovedItemsCount = itemsCount,
Message = $"{itemsCount} آیتم از سبد خرید حذف شد"
};
}
}

View File

@@ -0,0 +1,11 @@
namespace CMSMicroservice.Application.UserCartsCQ.Commands.ClearCart;
public class ClearCartCommandValidator : AbstractValidator<ClearCartCommand>
{
public ClearCartCommandValidator()
{
RuleFor(v => v.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
}
}

View File

@@ -0,0 +1,8 @@
namespace CMSMicroservice.Application.UserCartsCQ.Commands.ClearCart;
public class ClearCartResponseDto
{
public long UserId { get; set; }
public int RemovedItemsCount { get; set; }
public string Message { get; set; }
}

View File

@@ -0,0 +1,23 @@
using Microsoft.Extensions.Logging;
using CMSMicroservice.Domain.Events;
namespace CMSMicroservice.Application.UserCartsCQ.EventHandlers.ClearCartEventHandlers;
public class ClearCartEventHandler : INotificationHandler<ClearCartEvent>
{
private readonly ILogger<ClearCartEventHandler> _logger;
public ClearCartEventHandler(ILogger<ClearCartEventHandler> logger)
{
_logger = logger;
}
public Task Handle(ClearCartEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Cart item {CartId} removed for user {UserId}",
notification.Item.Id,
notification.Item.UserId);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,24 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.UserOrderCQ.Commands.CancelOrder;
/// <summary>
/// Command برای لغو سفارش
/// </summary>
public record CancelOrderCommand : IRequest<CancelOrderResponseDto>
{
/// <summary>
/// شناسه سفارش
/// </summary>
public long OrderId { get; init; }
/// <summary>
/// دلیل لغو سفارش
/// </summary>
public string CancelReason { get; init; }
/// <summary>
/// آیا مبلغ باید بازگردانده شود؟
/// </summary>
public bool RefundPayment { get; init; }
}

View File

@@ -0,0 +1,74 @@
using CMSMicroservice.Domain.Events;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.UserOrderCQ.Commands.CancelOrder;
public class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, CancelOrderResponseDto>
{
private readonly IApplicationDbContext _context;
public CancelOrderCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<CancelOrderResponseDto> Handle(CancelOrderCommand request, CancellationToken cancellationToken)
{
// پیدا کردن سفارش
var order = await _context.UserOrders
.Include(o => o.Transaction)
.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
if (order == null)
{
throw new NotFoundException(nameof(UserOrder), request.OrderId);
}
// چک کردن که سفارش قابل لغو باشد
if (order.DeliveryStatus == DeliveryStatus.Delivered)
{
throw new InvalidOperationException("سفارش تحویل داده شده قابل لغو نیست");
}
if (order.DeliveryStatus == DeliveryStatus.Cancelled)
{
throw new InvalidOperationException("این سفارش قبلاً لغو شده است");
}
// تغییر وضعیت سفارش
order.DeliveryStatus = DeliveryStatus.Cancelled;
order.DeliveryDescription = $"لغو شده: {request.CancelReason}";
// اگر درخواست بازگشت پول داریم و پرداخت موفق بوده
if (request.RefundPayment &&
order.Transaction != null &&
order.Transaction.PaymentStatus == PaymentStatus.Success)
{
// ایجاد تراکنش استرداد
var refundTransaction = new Transactions
{
Amount = -order.Amount,
Description = $"بازگشت وجه سفارش {request.OrderId}: {request.CancelReason}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow,
RefId = $"REFUND-ORDER-{order.Id}",
Type = TransactionType.Buy
};
await _context.Transactionss.AddAsync(refundTransaction, cancellationToken);
}
// ثبت Event
order.AddDomainEvent(new CancelOrderEvent(order, request.CancelReason));
await _context.SaveChangesAsync(cancellationToken);
return new CancelOrderResponseDto
{
OrderId = order.Id,
Status = order.DeliveryStatus,
Message = "سفارش با موفقیت لغو شد",
RefundProcessed = request.RefundPayment && order.Transaction != null
};
}
}

View File

@@ -0,0 +1,17 @@
namespace CMSMicroservice.Application.UserOrderCQ.Commands.CancelOrder;
public class CancelOrderCommandValidator : AbstractValidator<CancelOrderCommand>
{
public CancelOrderCommandValidator()
{
RuleFor(v => v.OrderId)
.GreaterThan(0)
.WithMessage("شناسه سفارش باید بزرگتر از صفر باشد");
RuleFor(v => v.CancelReason)
.NotEmpty()
.WithMessage("دلیل لغو سفارش الزامی است")
.MaximumLength(500)
.WithMessage("دلیل لغو نباید بیش از 500 کاراکتر باشد");
}
}

View File

@@ -0,0 +1,11 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.UserOrderCQ.Commands.CancelOrder;
public class CancelOrderResponseDto
{
public long OrderId { get; set; }
public DeliveryStatus Status { get; set; }
public string Message { get; set; }
public bool RefundProcessed { get; set; }
}

View File

@@ -0,0 +1,26 @@
using Microsoft.Extensions.Logging;
using CMSMicroservice.Domain.Events;
namespace CMSMicroservice.Application.UserOrderCQ.EventHandlers.CancelOrderEventHandlers;
public class CancelOrderEventHandler : INotificationHandler<CancelOrderEvent>
{
private readonly ILogger<CancelOrderEventHandler> _logger;
public CancelOrderEventHandler(ILogger<CancelOrderEventHandler> logger)
{
_logger = logger;
}
public Task Handle(CancelOrderEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Order {OrderId} cancelled. Reason: {Reason}",
notification.Order.Id,
notification.CancelReason);
// اینجا می‌تونیم اعلان به کاربر بفرستیم
// یا موجودی محصولات رو بازگردانیم به انبار
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,21 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using MediatR;
namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeDiscountWallet;
/// <summary>
/// دستور شارژ کیف پول تخفیفی از طریق درگاه
/// </summary>
public class ChargeDiscountWalletCommand : IRequest<PaymentInitiateResult>
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; set; }
/// <summary>
/// مبلغ مورد نظر برای شارژ (ریال)
/// </summary>
public long Amount { get; set; }
}

View File

@@ -0,0 +1,101 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeDiscountWallet;
public class ChargeDiscountWalletCommandHandler
: IRequestHandler<ChargeDiscountWalletCommand, PaymentInitiateResult>
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ILogger<ChargeDiscountWalletCommandHandler> _logger;
public ChargeDiscountWalletCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
ILogger<ChargeDiscountWalletCommandHandler> logger)
{
_context = context;
_paymentGateway = paymentGateway;
_logger = logger;
}
public async Task<PaymentInitiateResult> Handle(
ChargeDiscountWalletCommand request,
CancellationToken cancellationToken)
{
try
{
_logger.LogInformation(
"Charging discount wallet 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. ایجاد درخواست پرداخت
var paymentRequest = new PaymentRequest
{
Amount = request.Amount,
UserId = user.Id,
Mobile = user.Mobile ?? "",
CallbackUrl = $"https://yourdomain.com/api/wallet/verify-discount-charge",
Description = $"شارژ کیف پول تخفیفی - کاربر {user.Id}"
};
var paymentResult = await _paymentGateway.InitiatePaymentAsync(paymentRequest);
if (!paymentResult.IsSuccess)
{
_logger.LogError(
"Payment gateway failed for UserId {UserId}: {ErrorMessage}",
user.Id,
paymentResult.ErrorMessage
);
throw new Exception($"خطا در ارتباط با درگاه پرداخت: {paymentResult.ErrorMessage}");
}
_logger.LogInformation(
"Discount wallet charge initiated. UserId: {UserId}, RefId: {RefId}",
user.Id,
paymentResult.RefId
);
return paymentResult;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in ChargeDiscountWalletCommand for UserId: {UserId}",
request.UserId
);
throw;
}
}
}

View File

@@ -0,0 +1,19 @@
using FluentValidation;
namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeDiscountWallet;
public class ChargeDiscountWalletCommandValidator : AbstractValidator<ChargeDiscountWalletCommand>
{
public ChargeDiscountWalletCommandValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
RuleFor(x => x.Amount)
.GreaterThanOrEqualTo(10_000)
.WithMessage("حداقل مبلغ شارژ 10,000 تومان است")
.LessThanOrEqualTo(1_000_000_000)
.WithMessage("حداکثر مبلغ شارژ 1,000,000,000 تومان است");
}
}

View File

@@ -0,0 +1,26 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using MediatR;
namespace CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge;
/// <summary>
/// دستور تأیید شارژ کیف پول تخفیفی
/// </summary>
public class VerifyDiscountWalletChargeCommand : IRequest<bool>
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; set; }
/// <summary>
/// مبلغ
/// </summary>
public long Amount { get; set; }
/// <summary>
/// کد Authority از درگاه
/// </summary>
public string Authority { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,122 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge;
public class VerifyDiscountWalletChargeCommandHandler
: IRequestHandler<VerifyDiscountWalletChargeCommand, bool>
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ILogger<VerifyDiscountWalletChargeCommandHandler> _logger;
public VerifyDiscountWalletChargeCommandHandler(
IApplicationDbContext context,
IPaymentGatewayService paymentGateway,
ILogger<VerifyDiscountWalletChargeCommandHandler> logger)
{
_context = context;
_paymentGateway = paymentGateway;
_logger = logger;
}
public async Task<bool> Handle(
VerifyDiscountWalletChargeCommand request,
CancellationToken cancellationToken)
{
try
{
_logger.LogInformation(
"Verifying discount wallet charge. UserId: {UserId}, Amount: {Amount}, Authority: {Authority}",
request.UserId,
request.Amount,
request.Authority
);
// 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. Verify با درگاه
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority,
request.Authority // verificationToken - در بعضی درگاه‌ها مثل زرین‌پال همان Authority است
);
if (!verifyResult.IsSuccess)
{
_logger.LogWarning(
"Discount wallet charge verification failed for UserId {UserId}: {Message}",
request.UserId,
verifyResult.Message
);
throw new Exception($"تراکنش ناموفق: {verifyResult.Message}");
}
// 3. شارژ DiscountBalance
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == user.Id, cancellationToken);
if (wallet == null)
{
_logger.LogError("Wallet not found for UserId: {UserId}", request.UserId);
throw new NotFoundException($"کیف پول کاربر با شناسه {request.UserId} یافت نشد");
}
var oldBalance = wallet.DiscountBalance;
wallet.DiscountBalance += request.Amount;
_logger.LogInformation(
"Charging discount balance for UserId {UserId}: {OldBalance} -> {NewBalance}",
request.UserId,
oldBalance,
wallet.DiscountBalance
);
// 4. ثبت Transaction
var transaction = new Transactions
{
Amount = request.Amount,
Description = $"شارژ کیف پول تخفیفی - کاربر {user.Id}",
PaymentStatus = PaymentStatus.Success,
PaymentDate = DateTime.UtcNow,
RefId = verifyResult.RefId,
Type = TransactionType.DiscountWalletCharge
};
_context.Transactionss.Add(transaction);
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Discount wallet charged successfully. UserId: {UserId}, TransactionId: {TransactionId}, RefId: {RefId}",
user.Id,
transaction.Id,
verifyResult.RefId
);
return true;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in VerifyDiscountWalletChargeCommand for UserId: {UserId}",
request.UserId
);
throw;
}
}
}

View File

@@ -35,6 +35,11 @@ public class ClubMembership : BaseAuditableEntity
/// </summary>
public long TotalEarned { get; set; }
/// <summary>
/// نحوه خرید پکیج که منجر به فعالسازی باشگاه شد
/// </summary>
public PackagePurchaseMethod PurchaseMethod { get; set; }
/// <summary>
/// UserClubFeature Collection Navigation Reference
/// </summary>

View File

@@ -85,6 +85,21 @@ public class UserCommissionPayout : BaseAuditableEntity
/// </summary>
public string? RejectionReason { get; set; }
/// <summary>
/// شماره مرجع بانک (بعد از واریز موفق)
/// </summary>
public string? BankReferenceId { get; set; }
/// <summary>
/// کد پیگیری بانکی
/// </summary>
public string? BankTrackingCode { get; set; }
/// <summary>
/// دلیل خطا در پرداخت (اگر ناموفق باشد)
/// </summary>
public string? PaymentFailureReason { get; set; }
/// <summary>
/// CommissionPayoutHistory Collection Navigation Reference
/// </summary>

View File

@@ -0,0 +1,59 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Domain.Entities;
/// <summary>
/// قرارداد وام دایا
/// </summary>
public class DayaLoanContract : BaseAuditableEntity
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; set; }
/// <summary>
/// User Navigation Property
/// </summary>
public virtual User User { get; set; }
/// <summary>
/// کد ملی
/// </summary>
public string NationalCode { get; set; }
/// <summary>
/// شماره قرارداد دایا
/// </summary>
public string? ContractNumber { get; set; }
/// <summary>
/// وضعیت وام
/// </summary>
public DayaLoanStatus Status { get; set; }
/// <summary>
/// آیا پردازش شده است؟ (شارژ کیف پول انجام شده)
/// </summary>
public bool IsProcessed { get; set; }
/// <summary>
/// تاریخ آخرین استعلام
/// </summary>
public DateTime? LastCheckDate { get; set; }
/// <summary>
/// تاریخ پردازش
/// </summary>
public DateTime? ProcessedDate { get; set; }
/// <summary>
/// شناسه تراکنش (بعد از پردازش)
/// </summary>
public long? TransactionId { get; set; }
/// <summary>
/// Transaction Navigation Property
/// </summary>
public virtual Transactions? Transaction { get; set; }
}

View File

@@ -8,6 +8,8 @@ public class User : BaseAuditableEntity
public string? LastName { get; set; }
//شماره موبایل
public string Mobile { get; set; }
//ایمیل
public string? Email { get; set; }
//کد ملی
public string? NationalCode { get; set; }
//آدرس آواتار
@@ -54,6 +56,21 @@ public class User : BaseAuditableEntity
/// </summary>
public NetworkLeg? LegPosition { get; set; }
/// <summary>
/// آیا اعتبار دایا را دریافت کرده است؟
/// </summary>
public bool HasReceivedDayaCredit { get; set; }
/// <summary>
/// تاریخ دریافت اعتبار دایا
/// </summary>
public DateTime? DayaCreditReceivedAt { get; set; }
/// <summary>
/// نحوه خرید پکیج طلایی (برای جلوگیری از خرید مجدد)
/// </summary>
public PackagePurchaseMethod PackagePurchaseMethod { get; set; } = PackagePurchaseMethod.None;
// ============= Navigation Properties =============
//UserAddress Collection Navigation Reference
@@ -80,4 +97,6 @@ public class User : BaseAuditableEntity
public virtual ICollection<NetworkWeeklyBalance>? NetworkWeeklyBalances { get; set; }
//UserCommissionPayout Collection Navigation Reference
public virtual ICollection<UserCommissionPayout>? CommissionPayouts { get; set; }
//DayaLoanContract Collection Navigation Reference
public virtual ICollection<DayaLoanContract>? DayaLoanContracts { get; set; }
}

View File

@@ -14,6 +14,10 @@ public class UserWalletChangeLog : BaseAuditableEntity
public long CurrentNetworkBalance { get; set; }
//تغییر موجودی شبکه
public long ChangeNerworkValue { get; set; }
//موجودی جاری تخفیف
public long CurrentDiscountBalance { get; set; }
//تغییر موجودی تخفیف
public long ChangeDiscountValue { get; set; }
//افزایشی؟
public bool IsIncrease { get; set; }
//شناسه ارجاع

View File

@@ -25,8 +25,13 @@ public enum CommissionPayoutStatus
/// </summary>
Withdrawn = 3,
/// <summary>
/// خطا در پرداخت بانکی
/// </summary>
PaymentFailed = 4,
/// <summary>
/// لغو شده
/// </summary>
Cancelled = 4
Cancelled = 5
}

View File

@@ -0,0 +1,22 @@
namespace CMSMicroservice.Domain.Enums;
/// <summary>
/// وضعیت وام دایا
/// </summary>
public enum DayaLoanStatus
{
/// <summary>
/// در انتظار دریافت وام (خرید انجام شده، قرارداد امضا شده، درخواست وام ثبت شده)
/// </summary>
PendingReceive = 0,
/// <summary>
/// وام دریافت شده (در آینده اضافه می‌شود)
/// </summary>
Received = 1,
/// <summary>
/// رد شده (در آینده اضافه می‌شود)
/// </summary>
Rejected = 2,
}

View File

@@ -13,5 +13,7 @@ public enum DeliveryStatus
Delivered = 3,
// مرجوع شده
Returned = 4,
// لغو شده
Cancelled = 5,
}

View File

@@ -0,0 +1,22 @@
namespace CMSMicroservice.Domain.Enums;
/// <summary>
/// نحوه خرید پکیج طلایی توسط کاربر
/// </summary>
public enum PackagePurchaseMethod
{
/// <summary>
/// هنوز پکیج خریداری نکرده
/// </summary>
None = 0,
/// <summary>
/// از طریق وام دایا
/// </summary>
DayaLoan = 1,
/// <summary>
/// از طریق پرداخت مستقیم درگاه بانکی
/// </summary>
DirectPurchase = 2
}

View File

@@ -0,0 +1,13 @@
namespace CMSMicroservice.Domain.Events;
public class CancelOrderEvent : BaseEvent
{
public CancelOrderEvent(UserOrder order, string reason)
{
Order = order;
CancelReason = reason;
}
public UserOrder Order { get; }
public string CancelReason { get; }
}

View File

@@ -0,0 +1,11 @@
namespace CMSMicroservice.Domain.Events;
public class ClearCartEvent : BaseEvent
{
public ClearCartEvent(UserCarts item)
{
Item = item;
}
public UserCarts Item { get; }
}

View File

@@ -0,0 +1,15 @@
namespace CMSMicroservice.Domain.Events;
public class DayaLoanApprovedEvent : BaseEvent
{
public DayaLoanApprovedEvent(User user, Transactions transaction, string contractNumber)
{
User = user;
Transaction = transaction;
ContractNumber = contractNumber;
}
public User User { get; }
public Transactions Transaction { get; }
public string ContractNumber { get; }
}

View File

@@ -0,0 +1,13 @@
namespace CMSMicroservice.Domain.Events;
public class RefundTransactionEvent : BaseEvent
{
public RefundTransactionEvent(Transactions refundTransaction, Transactions originalTransaction)
{
RefundTransaction = refundTransaction;
OriginalTransaction = originalTransaction;
}
public Transactions RefundTransaction { get; }
public Transactions OriginalTransaction { get; }
}

View File

@@ -0,0 +1,11 @@
namespace CMSMicroservice.Domain.Events;
public class VerifyTransactionEvent : BaseEvent
{
public VerifyTransactionEvent(Transactions item)
{
Item = item;
}
public Transactions Item { get; }
}

View File

@@ -1,9 +1,11 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.DayaLoanCQ.Services;
using CMSMicroservice.Infrastructure.Persistence;
using CMSMicroservice.Infrastructure.Persistence.Interceptors;
using CMSMicroservice.Infrastructure.BackgroundJobs;
using CMSMicroservice.Infrastructure.Services.Monitoring;
using CMSMicroservice.Infrastructure.Configuration;
using CMSMicroservice.Infrastructure.Services.Payment;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -30,6 +32,37 @@ public static class ConfigureServices
services.AddScoped<INetworkPlacementService, NetworkPlacementService>();
services.AddScoped<IAlertService, AlertService>();
services.AddScoped<IUserNotificationService, UserNotificationService>();
services.AddScoped<IDayaLoanApiService, MockDayaLoanApiService>(); // Mock - جایگزین با Real برای Production
// Payment Gateway Service - برای Development از Mock استفاده می‌شود
// برای Production یکی از سرویس‌های واقعی را فعال کنید
var useRealPaymentGateway = configuration.GetValue<bool>("UseRealPaymentGateway", false);
if (useRealPaymentGateway)
{
var paymentProvider = configuration.GetValue<string>("PaymentProvider", "BankMellat");
if (paymentProvider == "Daya")
{
services.AddHttpClient<IPaymentGatewayService, DayaPaymentService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
}
else if (paymentProvider == "BankMellat")
{
services.AddHttpClient<IPaymentGatewayService, BankMellatPaymentService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
}
else
{
throw new InvalidOperationException($"Invalid PaymentProvider: {paymentProvider}. Valid values: Daya, BankMellat");
}
}
else
{
// Mock برای Development و Testing
services.AddScoped<IPaymentGatewayService, MockPaymentGatewayService>();
}
services.AddScoped<IApplicationDbContext>(p => p.GetRequiredService<ApplicationDbContext>());
// Background Workers - Deprecated: Using Hangfire instead

View File

@@ -64,6 +64,7 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
public DbSet<UserOrder> UserOrders => Set<UserOrder>();
public DbSet<UserWallet> UserWallets => Set<UserWallet>();
public DbSet<UserWalletChangeLog> UserWalletChangeLogs => Set<UserWalletChangeLog>();
public DbSet<DayaLoanContract> DayaLoanContracts => Set<DayaLoanContract>();
// ============= Network Club System DbSets =============

View File

@@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class UpdatePoolContributionPercent : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// تغییر درصد استخر از 10% به 20%
migrationBuilder.Sql(@"
UPDATE SystemConfigurations
SET Value = '20',
Description = N'درصد مشارکت در استخر هفتگی از کل فعال‌سازی‌های جدید شبکه (20%)'
WHERE [Key] = 'Commission.WeeklyPoolContributionPercent'
");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// بازگشت به 10%
migrationBuilder.Sql(@"
UPDATE SystemConfigurations
SET Value = '10',
Description = N'درصد مشارکت در استخر هفتگی از تعادل کل (در صورت نیاز)'
WHERE [Key] = 'Commission.WeeklyPoolContributionPercent'
");
}
}
}

View File

@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddEmailToUser : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Email",
schema: "CMS",
table: "Users",
type: "nvarchar(max)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Email",
schema: "CMS",
table: "Users");
}
}
}

View File

@@ -0,0 +1,99 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddDayaLoanIntegration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "DayaCreditReceivedAt",
schema: "CMS",
table: "Users",
type: "datetime2",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "HasReceivedDayaCredit",
schema: "CMS",
table: "Users",
type: "bit",
nullable: false,
defaultValue: false);
migrationBuilder.CreateTable(
name: "DayaLoanContracts",
schema: "CMS",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
UserId = table.Column<long>(type: "bigint", nullable: false),
NationalCode = table.Column<string>(type: "nvarchar(max)", nullable: false),
ContractNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
Status = table.Column<int>(type: "int", nullable: false),
IsProcessed = table.Column<bool>(type: "bit", nullable: false),
LastCheckDate = table.Column<DateTime>(type: "datetime2", nullable: true),
ProcessedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
TransactionId = table.Column<long>(type: "bigint", nullable: true),
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DayaLoanContracts", x => x.Id);
table.ForeignKey(
name: "FK_DayaLoanContracts_Transactionss_TransactionId",
column: x => x.TransactionId,
principalSchema: "CMS",
principalTable: "Transactionss",
principalColumn: "Id");
table.ForeignKey(
name: "FK_DayaLoanContracts_Users_UserId",
column: x => x.UserId,
principalSchema: "CMS",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_DayaLoanContracts_TransactionId",
schema: "CMS",
table: "DayaLoanContracts",
column: "TransactionId");
migrationBuilder.CreateIndex(
name: "IX_DayaLoanContracts_UserId",
schema: "CMS",
table: "DayaLoanContracts",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DayaLoanContracts",
schema: "CMS");
migrationBuilder.DropColumn(
name: "DayaCreditReceivedAt",
schema: "CMS",
table: "Users");
migrationBuilder.DropColumn(
name: "HasReceivedDayaCredit",
schema: "CMS",
table: "Users");
}
}
}

View File

@@ -0,0 +1,80 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddPackagePurchaseMethod : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "PackagePurchaseMethod",
schema: "CMS",
table: "Users",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "BankReferenceId",
schema: "CMS",
table: "UserCommissionPayouts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "BankTrackingCode",
schema: "CMS",
table: "UserCommissionPayouts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PaymentFailureReason",
schema: "CMS",
table: "UserCommissionPayouts",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "PurchaseMethod",
schema: "CMS",
table: "ClubMemberships",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PackagePurchaseMethod",
schema: "CMS",
table: "Users");
migrationBuilder.DropColumn(
name: "BankReferenceId",
schema: "CMS",
table: "UserCommissionPayouts");
migrationBuilder.DropColumn(
name: "BankTrackingCode",
schema: "CMS",
table: "UserCommissionPayouts");
migrationBuilder.DropColumn(
name: "PaymentFailureReason",
schema: "CMS",
table: "UserCommissionPayouts");
migrationBuilder.DropColumn(
name: "PurchaseMethod",
schema: "CMS",
table: "ClubMemberships");
}
}
}

View File

@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddDiscountBalanceToWalletChangeLog : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "ChangeDiscountValue",
schema: "CMS",
table: "UserWalletChangeLogs",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.AddColumn<long>(
name: "CurrentDiscountBalance",
schema: "CMS",
table: "UserWalletChangeLogs",
type: "bigint",
nullable: false,
defaultValue: 0L);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ChangeDiscountValue",
schema: "CMS",
table: "UserWalletChangeLogs");
migrationBuilder.DropColumn(
name: "CurrentDiscountBalance",
schema: "CMS",
table: "UserWalletChangeLogs");
}
}
}

View File

@@ -157,6 +157,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<int>("PurchaseMethod")
.HasColumnType("int");
b.Property<long>("TotalEarned")
.HasColumnType("bigint");
@@ -239,6 +242,12 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<int>("BalancesEarned")
.HasColumnType("int");
b.Property<string>("BankReferenceId")
.HasColumnType("nvarchar(max)");
b.Property<string>("BankTrackingCode")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
@@ -261,6 +270,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<DateTime?>("PaidAt")
.HasColumnType("datetime2");
b.Property<string>("PaymentFailureReason")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("ProcessedAt")
.HasColumnType("datetime2");
@@ -543,6 +555,63 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.ToTable("Contracts", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.DayaLoanContract", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("ContractNumber")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<bool>("IsProcessed")
.HasColumnType("bit");
b.Property<DateTime?>("LastCheckDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("LastModified")
.HasColumnType("datetime2");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("NationalCode")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("ProcessedDate")
.HasColumnType("datetime2");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<long?>("TransactionId")
.HasColumnType("bigint");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("TransactionId");
b.HasIndex("UserId");
b.ToTable("DayaLoanContracts", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.FactorDetails", b =>
{
b.Property<long>("Id")
@@ -1420,12 +1489,21 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("DayaCreditReceivedAt")
.HasColumnType("datetime2");
b.Property<string>("Email")
.HasColumnType("nvarchar(max)");
b.Property<bool>("EmailNotifications")
.HasColumnType("bit");
b.Property<string>("FirstName")
.HasColumnType("nvarchar(max)");
b.Property<bool>("HasReceivedDayaCredit")
.HasColumnType("bit");
b.Property<string>("HashPassword")
.HasColumnType("nvarchar(max)");
@@ -1463,6 +1541,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<long?>("NetworkParentId")
.HasColumnType("bigint");
b.Property<int>("PackagePurchaseMethod")
.HasColumnType("int");
b.Property<long?>("ParentId")
.HasColumnType("bigint");
@@ -1787,6 +1868,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<long>("ChangeDiscountValue")
.HasColumnType("bigint");
b.Property<long>("ChangeNerworkValue")
.HasColumnType("bigint");
@@ -1802,6 +1886,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<long>("CurrentBalance")
.HasColumnType("bigint");
b.Property<long>("CurrentDiscountBalance")
.HasColumnType("bigint");
b.Property<long>("CurrentNetworkBalance")
.HasColumnType("bigint");
@@ -1896,6 +1983,23 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Navigation("WeeklyPool");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.DayaLoanContract", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.Transactions", "Transaction")
.WithMany()
.HasForeignKey("TransactionId");
b.HasOne("CMSMicroservice.Domain.Entities.User", "User")
.WithMany("DayaLoanContracts")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Transaction");
b.Navigation("User");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.FactorDetails", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.UserOrder", "Order")
@@ -2236,6 +2340,8 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Navigation("CommissionPayouts");
b.Navigation("DayaLoanContracts");
b.Navigation("NetworkChildren");
b.Navigation("NetworkWeeklyBalances");

View File

@@ -0,0 +1,104 @@
using CMSMicroservice.Application.DayaLoanCQ.Services;
using CMSMicroservice.Domain.Enums;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace CMSMicroservice.Infrastructure.Services;
/// <summary>
/// Mock Implementation برای شبیه‌سازی Daya API
/// این کلاس فقط برای تست و توسعه است و باید با Implementation واقعی جایگزین شود
/// </summary>
public class MockDayaLoanApiService : IDayaLoanApiService
{
private readonly ILogger<MockDayaLoanApiService> _logger;
public MockDayaLoanApiService(ILogger<MockDayaLoanApiService> logger)
{
_logger = logger;
}
public async Task<List<DayaLoanStatusResult>> CheckLoanStatusAsync(
List<string> nationalCodes,
CancellationToken cancellationToken = default)
{
_logger.LogWarning("⚠️ Using MOCK Daya API Service - Replace with real implementation!");
// شبیه‌سازی تاخیر شبکه
await Task.Delay(100, cancellationToken);
var results = new List<DayaLoanStatusResult>();
foreach (var nationalCode in nationalCodes)
{
// شبیه‌سازی: کدملی‌هایی که با 1 شروع می‌شوند وام گرفته‌اند
if (nationalCode.StartsWith("1"))
{
results.Add(new DayaLoanStatusResult
{
NationalCode = nationalCode,
Status = DayaLoanStatus.PendingReceive,
ContractNumber = $"MOCK-DAYA-{nationalCode}-{DateTime.Now.Ticks}"
});
}
// شبیه‌سازی: کدملی‌هایی که با 2 شروع می‌شوند رد شده‌اند
else if (nationalCode.StartsWith("2"))
{
results.Add(new DayaLoanStatusResult
{
NationalCode = nationalCode,
Status = DayaLoanStatus.Rejected,
ContractNumber = null
});
}
// بقیه: هنوز بررسی نشده‌اند
else
{
results.Add(new DayaLoanStatusResult
{
NationalCode = nationalCode,
Status = DayaLoanStatus.PendingReceive,
ContractNumber = null // هنوز قرارداد صادر نشده
});
}
}
_logger.LogInformation("Mock Daya API returned {Count} results", results.Count);
return results;
}
}
/// <summary>
/// Real Implementation برای API واقعی دایا
/// TODO: این کلاس باید پیاده‌سازی شود زمانی که API دایا آماده شد
/// </summary>
public class DayaLoanApiService : IDayaLoanApiService
{
private readonly HttpClient _httpClient;
private readonly ILogger<DayaLoanApiService> _logger;
public DayaLoanApiService(HttpClient httpClient, ILogger<DayaLoanApiService> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<List<DayaLoanStatusResult>> CheckLoanStatusAsync(
List<string> nationalCodes,
CancellationToken cancellationToken = default)
{
// TODO: پیاده‌سازی واقعی API دایا
// مثال:
// var request = new DayaApiRequest { NationalCodes = nationalCodes };
// var response = await _httpClient.PostAsJsonAsync("/api/loan/check", request, cancellationToken);
// response.EnsureSuccessStatusCode();
// var result = await response.Content.ReadFromJsonAsync<DayaApiResponse>(cancellationToken);
// return MapToResults(result);
throw new NotImplementedException("Real Daya API is not implemented yet. Use MockDayaLoanApiService for testing.");
}
}

View File

@@ -70,11 +70,25 @@ public class UserNotificationService : IUserNotificationService
var formattedAmount = amount.ToString("N0", new System.Globalization.CultureInfo("fa-IR"));
// Send Email (TODO: User entity needs Email field)
// if (_emailSettings.Enabled && !string.IsNullOrEmpty(user.Email))
// {
// await SendEmailAsync(...);
// }
// Send Email
if (_emailSettings.Enabled && !string.IsNullOrEmpty(user.Email))
{
var emailSubject = $"واریز کمیسیون هفته {weekNumber}";
var emailBody = "<div dir='rtl' style='font-family: Tahoma, Arial; text-align: right;'>" +
$"<h2>سلام {userFullName}</h2>" +
$"<p>کمیسیون هفته {weekNumber} شما به مبلغ <strong>{formattedAmount} ریال</strong> به کیف پول شما واریز شد.</p>" +
"<p>از اعتماد شما سپاسگزاریم.</p>" +
"<hr/>" +
"<p style='color: #666; font-size: 12px;'>FourSat - سیستم مدیریت باشگاه مشتریان</p>" +
"</div>";
await SendEmailAsync(
toEmail: user.Email,
toName: userFullName,
subject: emailSubject,
body: emailBody,
cancellationToken: cancellationToken);
}
// Send SMS
if (_smsSettings.Enabled && !string.IsNullOrEmpty(user.Mobile))
@@ -107,11 +121,25 @@ public class UserNotificationService : IUserNotificationService
var userFullName = $"{user.FirstName} {user.LastName}".Trim();
if (string.IsNullOrEmpty(userFullName)) userFullName = "کاربر عزیز";
// Send Email (TODO: User entity needs Email field)
// if (_emailSettings.Enabled && !string.IsNullOrEmpty(user.Email))
// {
// await SendEmailAsync(...);
// }
// Send Email
if (_emailSettings.Enabled && !string.IsNullOrEmpty(user.Email))
{
var emailSubject = "فعال‌سازی باشگاه مشتریان FourSat";
var emailBody = "<div dir='rtl' style='font-family: Tahoma, Arial; text-align: right;'>" +
$"<h2>تبریک {userFullName}!</h2>" +
"<p>عضویت شما در <strong>باشگاه مشتریان FourSat</strong> با موفقیت فعال شد.</p>" +
"<p>از این پس می‌توانید از مزایای ویژه باشگاه بهره‌مند شوید.</p>" +
"<hr/>" +
"<p style='color: #666; font-size: 12px;'>FourSat - سیستم مدیریت باشگاه مشتریان</p>" +
"</div>";
await SendEmailAsync(
toEmail: user.Email,
toName: userFullName,
subject: emailSubject,
body: emailBody,
cancellationToken: cancellationToken);
}
// Send SMS
if (_smsSettings.Enabled && !string.IsNullOrEmpty(user.Mobile))
@@ -145,11 +173,35 @@ public class UserNotificationService : IUserNotificationService
var userFullName = $"{user.FirstName} {user.LastName}".Trim();
if (string.IsNullOrEmpty(userFullName)) userFullName = "کاربر عزیز";
// Send Email (TODO: User entity needs Email field)
// if (_emailSettings.Enabled && !string.IsNullOrEmpty(user.Email))
// {
// await SendEmailAsync(...);
// }
// Send Email
if (_emailSettings.Enabled && !string.IsNullOrEmpty(user.Email))
{
var emailSubject = "خطا در واریز کمیسیون";
var emailBody = "<div dir='rtl' style='font-family: Tahoma, Arial; text-align: right;'>" +
$"<h2>سلام {userFullName}</h2>" +
"<p>متأسفانه در واریز کمیسیون شما خطایی رخ داده است:</p>" +
$"<p style='color: red;'><strong>{errorMessage}</strong></p>" +
"<p>لطفاً با پشتیبانی تماس بگیرید.</p>" +
"<hr/>" +
"<p style='color: #666; font-size: 12px;'>FourSat - سیستم مدیریت باشگاه مشتریان</p>" +
"</div>";
await SendEmailAsync(
toEmail: user.Email,
toName: userFullName,
subject: emailSubject,
body: emailBody,
cancellationToken: cancellationToken);
}
// Send SMS
if (_smsSettings.Enabled && !string.IsNullOrEmpty(user.Mobile))
{
await SendSmsAsync(
phoneNumber: user.Mobile,
message: $"خطا در واریز کمیسیون: {errorMessage}\nلطفاً با پشتیبانی تماس بگیرید.",
cancellationToken: cancellationToken);
}
}
catch (Exception ex)
{

View File

@@ -43,6 +43,18 @@ service TransactionsContract
};
};
rpc VerifyTransaction(VerifyTransactionRequest) returns (VerifyTransactionResponse){
option (google.api.http) = {
post: "/VerifyTransaction"
body: "*"
};
};
rpc RefundTransaction(RefundTransactionRequest) returns (RefundTransactionResponse){
option (google.api.http) = {
post: "/RefundTransaction"
body: "*"
};
};
}
message CreateNewTransactionsRequest
{
@@ -146,3 +158,36 @@ message GetAllTransactionsByFilterResponseModel
messages.TransactionType type = 7;
}
}
// VerifyTransaction Messages
message VerifyTransactionRequest
{
int64 transaction_id = 1;
string ref_id = 2;
messages.PaymentStatus status = 3;
google.protobuf.Timestamp payment_date = 4;
}
message VerifyTransactionResponse
{
int64 transaction_id = 1;
messages.PaymentStatus status = 2;
string ref_id = 3;
string message = 4;
}
// RefundTransaction Messages
message RefundTransactionRequest
{
int64 transaction_id = 1;
string refund_reason = 2;
google.protobuf.Int64Value refund_amount = 3;
}
message RefundTransactionResponse
{
int64 original_transaction_id = 1;
int64 refund_transaction_id = 2;
int64 refund_amount = 3;
string message = 4;
}

View File

@@ -67,13 +67,14 @@ message CreateNewUserRequest
google.protobuf.StringValue first_name = 1;
google.protobuf.StringValue last_name = 2;
string mobile = 3;
google.protobuf.StringValue national_code = 4;
google.protobuf.StringValue avatar_path = 5;
google.protobuf.Int64Value parent_id = 6;
bool email_notifications = 7;
bool sms_notifications = 8;
bool push_notifications = 9;
google.protobuf.Timestamp birth_date = 10;
google.protobuf.StringValue email = 4;
google.protobuf.StringValue national_code = 5;
google.protobuf.StringValue avatar_path = 6;
google.protobuf.Int64Value parent_id = 7;
bool email_notifications = 8;
bool sms_notifications = 9;
bool push_notifications = 10;
google.protobuf.Timestamp birth_date = 11;
}
message CreateNewUserResponse
{
@@ -84,14 +85,15 @@ message UpdateUserRequest
int64 id = 1;
google.protobuf.StringValue first_name = 2;
google.protobuf.StringValue last_name = 3;
google.protobuf.StringValue national_code = 4;
google.protobuf.StringValue avatar_path = 5;
bool is_rules_accepted = 6;
google.protobuf.Timestamp rules_accepted_at = 7;
bool email_notifications = 8;
bool sms_notifications = 9;
bool push_notifications = 10;
google.protobuf.Timestamp birth_date = 11;
google.protobuf.StringValue email = 4;
google.protobuf.StringValue national_code = 5;
google.protobuf.StringValue avatar_path = 6;
bool is_rules_accepted = 7;
google.protobuf.Timestamp rules_accepted_at = 8;
bool email_notifications = 9;
bool sms_notifications = 10;
bool push_notifications = 11;
google.protobuf.Timestamp birth_date = 12;
}
message DeleteUserRequest
{
@@ -107,16 +109,17 @@ message GetUserResponse
google.protobuf.StringValue first_name = 2;
google.protobuf.StringValue last_name = 3;
string mobile = 4;
google.protobuf.StringValue national_code = 5;
google.protobuf.StringValue avatar_path = 6;
google.protobuf.Int64Value parent_id = 7;
string referral_code = 8;
bool is_mobile_verified = 9;
google.protobuf.Timestamp mobile_verified_at = 10;
bool email_notifications = 11;
bool sms_notifications = 12;
bool push_notifications = 13;
google.protobuf.Timestamp birth_date = 14;
google.protobuf.StringValue email = 5;
google.protobuf.StringValue national_code = 6;
google.protobuf.StringValue avatar_path = 7;
google.protobuf.Int64Value parent_id = 8;
string referral_code = 9;
bool is_mobile_verified = 10;
google.protobuf.Timestamp mobile_verified_at = 11;
bool email_notifications = 12;
bool sms_notifications = 13;
bool push_notifications = 14;
google.protobuf.Timestamp birth_date = 15;
}
message GetAllUserByFilterRequest
{

View File

@@ -43,6 +43,12 @@ service UserCartsContract
};
};
rpc ClearCart(ClearCartRequest) returns (ClearCartResponse){
option (google.api.http) = {
post: "/ClearCart"
body: "*"
};
};
}
message CreateNewUserCartsRequest
{
@@ -105,3 +111,16 @@ message GetAllUserCartsByFilterResponseModel
string product_thumbnail_path = 9;
google.protobuf.Timestamp created = 10;
}
// ClearCart Messages
message ClearCartRequest
{
int64 user_id = 1;
}
message ClearCartResponse
{
int64 user_id = 1;
int32 removed_items_count = 2;
string message = 3;
}

View File

@@ -49,6 +49,12 @@ service UserOrderContract
body: "*"
};
};
rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse){
option (google.api.http) = {
post: "/CancelOrder"
body: "*"
};
};
}
message CreateNewUserOrderRequest
{
@@ -225,3 +231,19 @@ message SubmitShopBuyOrderResponse
{
int64 id = 1;
}
// CancelOrder Messages
message CancelOrderRequest
{
int64 order_id = 1;
string cancel_reason = 2;
bool refund_payment = 3;
}
message CancelOrderResponse
{
int64 order_id = 1;
messages.DeliveryStatus status = 2;
string message = 3;
bool refund_processed = 4;
}

View File

@@ -187,6 +187,10 @@ using (var scope = app.Services.CreateScope())
});
app.Logger.LogInformation("✅ Hangfire recurring job 'weekly-commission-calculation' registered (Cron: 5 0 * * 0 - Sunday 00:05 UTC)");
// Daya Loan Check: Every 15 minutes
CMSMicroservice.WebApi.Workers.DayaLoanCheckWorker.Schedule(recurringJobManager);
app.Logger.LogInformation("✅ Hangfire recurring job 'daya-loan-check' registered (Cron: */15 * * * * - Every 15 minutes)");
}
app.Run();

View File

@@ -5,6 +5,9 @@ using CMSMicroservice.Application.TransactionsCQ.Commands.UpdateTransactions;
using CMSMicroservice.Application.TransactionsCQ.Commands.DeleteTransactions;
using CMSMicroservice.Application.TransactionsCQ.Queries.GetTransactions;
using CMSMicroservice.Application.TransactionsCQ.Queries.GetAllTransactionsByFilter;
using CMSMicroservice.Application.TransactionsCQ.Commands.VerifyTransaction;
using CMSMicroservice.Application.TransactionsCQ.Commands.RefundTransaction;
namespace CMSMicroservice.WebApi.Services;
public class TransactionsService : TransactionsContract.TransactionsContractBase
{
@@ -34,4 +37,14 @@ public class TransactionsService : TransactionsContract.TransactionsContractBase
{
return await _dispatchRequestToCQRS.Handle<GetAllTransactionsByFilterRequest, GetAllTransactionsByFilterQuery, GetAllTransactionsByFilterResponse>(request, context);
}
public override async Task<VerifyTransactionResponse> VerifyTransaction(VerifyTransactionRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<VerifyTransactionRequest, VerifyTransactionCommand, VerifyTransactionResponse>(request, context);
}
public override async Task<RefundTransactionResponse> RefundTransaction(RefundTransactionRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<RefundTransactionRequest, RefundTransactionCommand, RefundTransactionResponse>(request, context);
}
}

View File

@@ -5,6 +5,8 @@ using CMSMicroservice.Application.UserCartsCQ.Commands.UpdateUserCarts;
using CMSMicroservice.Application.UserCartsCQ.Commands.DeleteUserCarts;
using CMSMicroservice.Application.UserCartsCQ.Queries.GetUserCarts;
using CMSMicroservice.Application.UserCartsCQ.Queries.GetAllUserCartsByFilter;
using CMSMicroservice.Application.UserCartsCQ.Commands.ClearCart;
namespace CMSMicroservice.WebApi.Services;
public class UserCartsService : UserCartsContract.UserCartsContractBase
{
@@ -34,4 +36,9 @@ public class UserCartsService : UserCartsContract.UserCartsContractBase
{
return await _dispatchRequestToCQRS.Handle<GetAllUserCartsByFilterRequest, GetAllUserCartsByFilterQuery, GetAllUserCartsByFilterResponse>(request, context);
}
public override async Task<ClearCartResponse> ClearCart(ClearCartRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ClearCartRequest, ClearCartCommand, ClearCartResponse>(request, context);
}
}

View File

@@ -6,6 +6,8 @@ using CMSMicroservice.Application.UserOrderCQ.Commands.DeleteUserOrder;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetUserOrder;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetAllUserOrderByFilter;
using CMSMicroservice.Application.UserOrderCQ.Commands.SubmitShopBuyOrder;
using CMSMicroservice.Application.UserOrderCQ.Commands.CancelOrder;
namespace CMSMicroservice.WebApi.Services;
public class UserOrderService : UserOrderContract.UserOrderContractBase
{
@@ -39,4 +41,9 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
{
return await _dispatchRequestToCQRS.Handle<SubmitShopBuyOrderRequest, SubmitShopBuyOrderCommand, SubmitShopBuyOrderResponse>(request, context);
}
public override async Task<CancelOrderResponse> CancelOrder(CancelOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CancelOrderRequest, CancelOrderCommand, CancelOrderResponse>(request, context);
}
}

View File

@@ -0,0 +1,121 @@
using Hangfire;
using MediatR;
using Microsoft.Extensions.Logging;
using CMSMicroservice.Application.DayaLoanCQ.Commands.CheckDayaLoanStatus;
using CMSMicroservice.Application.DayaLoanCQ.Commands.ProcessDayaLoanApproval;
using CMSMicroservice.Domain.Enums;
using Microsoft.EntityFrameworkCore;
using CMSMicroservice.Infrastructure.Persistence;
using System.Linq;
namespace CMSMicroservice.WebApi.Workers;
/// <summary>
/// Worker برای استعلام خودکار وضعیت وام دایا (هر 15 دقیقه)
/// </summary>
public class DayaLoanCheckWorker
{
private readonly IMediator _mediator;
private readonly ApplicationDbContext _context;
private readonly ILogger<DayaLoanCheckWorker> _logger;
public DayaLoanCheckWorker(
IMediator mediator,
ApplicationDbContext context,
ILogger<DayaLoanCheckWorker> logger)
{
_mediator = mediator;
_context = context;
_logger = logger;
}
/// <summary>
/// متد اصلی که توسط Hangfire فراخوانی می‌شود
/// </summary>
[AutomaticRetry(Attempts = 3)]
public async Task ExecuteAsync()
{
_logger.LogInformation("DayaLoanCheckWorker started at {Time}", DateTime.UtcNow);
try
{
// پیدا کردن کاربرانی که اعتبار دایا را دریافت نکرده‌اند
var pendingUsers = await _context.Users
.Where(u =>
u.HasReceivedDayaCredit == false &&
u.NationalCode != null &&
u.NationalCode != "")
.Select(u => new { u.Id, u.NationalCode })
.ToListAsync();
if (!pendingUsers.Any())
{
_logger.LogInformation("No pending users found for Daya loan check");
return;
}
_logger.LogInformation("Found {Count} users with pending Daya loan status", pendingUsers.Count);
// استعلام از دایا
var checkCommand = new CheckDayaLoanStatusCommand
{
NationalCodes = pendingUsers.Select(u => u.NationalCode).ToList()
};
var checkResult = await _mediator.Send(checkCommand);
// پردازش نتایج
foreach (var result in checkResult.Results)
{
// فقط وضعیت PendingReceive را پردازش می‌کنیم (یعنی وام درخواست شده)
if (result.Status == DayaLoanStatus.PendingReceive && !string.IsNullOrEmpty(result.ContractNumber))
{
var user = pendingUsers.FirstOrDefault(u => u.NationalCode == result.NationalCode);
if (user != null)
{
try
{
// پردازش تایید وام و شارژ کیف پول
var processCommand = new ProcessDayaLoanApprovalCommand
{
UserId = user.Id,
ContractNumber = result.ContractNumber
};
var processResult = await _mediator.Send(processCommand);
_logger.LogInformation("Daya loan processed for user {UserId}. Contract: {ContractNumber}",
user.Id, result.ContractNumber);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing Daya loan for user {UserId}", user.Id);
}
}
}
}
_logger.LogInformation("DayaLoanCheckWorker completed. Checked: {Total}, Processed: {Success}",
checkResult.TotalChecked, checkResult.SuccessCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in DayaLoanCheckWorker");
throw; // Hangfire will retry
}
}
/// <summary>
/// متد برای Schedule کردن Worker (هر 15 دقیقه)
/// </summary>
public static void Schedule(IRecurringJobManager recurringJobManager)
{
// هر 15 دقیقه: */15 * * * *
recurringJobManager.AddOrUpdate<DayaLoanCheckWorker>(
"daya-loan-check",
worker => worker.ExecuteAsync(),
"*/15 * * * *", // هر 15 دقیقه
TimeZoneInfo.Utc
);
}
}

View File

@@ -0,0 +1,34 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Server=YOUR_PRODUCTION_SERVER;Database=FourSat_CMS;User Id=YOUR_USER;Password=YOUR_PASSWORD;TrustServerCertificate=True;MultipleActiveResultSets=true"
},
"Email": {
"Enabled": true,
"SmtpHost": "smtp.gmail.com",
"SmtpPort": 587,
"SmtpUsername": "your-production-email@gmail.com",
"SmtpPassword": "your-gmail-app-password",
"FromEmail": "noreply@foursat.com",
"FromName": "FourSat CMS",
"EnableSsl": true
},
"Sms": {
"Enabled": true,
"Provider": "Kavenegar",
"KavenegarApiKey": "YOUR_PRODUCTION_KAVENEGAR_API_KEY",
"Sender": "10008663"
},
"Jwt": {
"Issuer": "https://api.foursat.com",
"Audience": "https://foursat.com",
"SecretKey": "YOUR_PRODUCTION_SECRET_KEY_MINIMUM_32_CHARACTERS_LONG"
},
"AllowedHosts": "*"
}

View File

@@ -1,4 +1,6 @@
{
"UseRealPaymentGateway": false,
"PaymentProvider": "BankMellat",
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
"JwtIssuer": "https://localhost",
"JwtAudience": "https://localhost",
@@ -39,10 +41,20 @@
"KavenegarApiKey": "YOUR_KAVENEGAR_API_KEY",
"Sender": "10008663"
},
"DayaPayment": {
"BaseUrl": "https://api.daya.ir",
"ApiKey": "YOUR_DAYA_API_KEY"
},
"BankMellat": {
"ServiceUrl": "https://bpm.shaparak.ir/pgwchannel/services/pgw",
"TerminalId": "YOUR_TERMINAL_ID",
"Username": "YOUR_USERNAME",
"Password": "YOUR_PASSWORD"
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
"Protocols": "Http1AndHttp2"
}
},
"Authentication": {