feat: Add ClearCart command and response, implement CancelOrder command with validation, and enhance DeliveryStatus and User models
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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 ?? "درخواست برداشت رد شد"
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = "اعتبار دایا با موفقیت دریافت شد"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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("مبلغ کیف پول باید بزرگتر از صفر باشد");
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("شناسه کاربر باید بزرگتر از صفر باشد");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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 = "استرداد با موفقیت انجام شد"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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("مبلغ استرداد باید بزرگتر از صفر باشد");
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
? "پرداخت با موفقیت انجام شد"
|
||||
: "پرداخت ناموفق بود"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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("تاریخ پرداخت الزامی است");
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
//آدرس آواتار
|
||||
|
||||
@@ -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; }
|
||||
//آدرس آواتار
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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} آیتم از سبد خرید حذف شد"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.ClearCart;
|
||||
|
||||
public class ClearCartCommandValidator : AbstractValidator<ClearCartCommand>
|
||||
{
|
||||
public ClearCartCommandValidator()
|
||||
{
|
||||
RuleFor(v => v.UserId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 کاراکتر باشد");
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 تومان است");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user