diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/AdminManualCreditCharge/AdminManualCreditChargeCommand.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/AdminManualCreditCharge/AdminManualCreditChargeCommand.cs new file mode 100644 index 0000000..3baa57a --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/AdminManualCreditCharge/AdminManualCreditChargeCommand.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace CMSMicroservice.Application.WalletCQ.Commands.AdminManualCreditCharge; + +public class AdminManualCreditChargeCommand : IRequest +{ + public long UserId { get; set; } + public long Amount { get; set; } + public string? Note { get; set; } + public string? PerformedByAdminId { get; set; } +} + +public class AdminManualCreditChargeResult +{ + public bool Success { get; set; } + public string Message { get; set; } = string.Empty; + public long TransactionId { get; set; } + public long NewBalance { get; set; } +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/AdminManualCreditCharge/AdminManualCreditChargeCommandHandler.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/AdminManualCreditCharge/AdminManualCreditChargeCommandHandler.cs new file mode 100644 index 0000000..b9e5039 --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/AdminManualCreditCharge/AdminManualCreditChargeCommandHandler.cs @@ -0,0 +1,105 @@ +using CMSMicroservice.Application.Common; +using CMSMicroservice.Application.Common.Exceptions; +using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Entities; +using CMSMicroservice.Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace CMSMicroservice.Application.WalletCQ.Commands.AdminManualCreditCharge; + +public class AdminManualCreditChargeCommandHandler + : IRequestHandler +{ + private readonly IApplicationDbContext _context; + private readonly ILogger _logger; + private readonly IUserPaymentLock _paymentLock; + + public AdminManualCreditChargeCommandHandler( + IApplicationDbContext context, + ILogger logger, + IUserPaymentLock paymentLock) + { + _context = context; + _logger = logger; + _paymentLock = paymentLock; + } + + public Task Handle( + AdminManualCreditChargeCommand request, + CancellationToken cancellationToken) => + _paymentLock.ExecuteAsync( + PaymentLockScopes.Initiate(request.UserId), + PaymentLockStrategy.FailFast, + ct => HandleCore(request, ct), + cancellationToken); + + private async Task HandleCore( + AdminManualCreditChargeCommand request, + CancellationToken cancellationToken) + { + var user = await _context.Users + .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken) + ?? throw new NotFoundException(nameof(User), request.UserId); + + var wallet = await _context.UserWallets + .FirstOrDefaultAsync(w => w.UserId == user.Id, cancellationToken) + ?? throw new NotFoundException($"کیف پول کاربر با شناسه {request.UserId} یافت نشد"); + + var oldBalance = wallet.Balance; + wallet.Balance += request.Amount; + + var refId = $"MANUAL-{Guid.NewGuid():N}"; + var notePart = string.IsNullOrWhiteSpace(request.Note) + ? string.Empty + : $" — {request.Note.Trim()}"; + var adminPart = string.IsNullOrWhiteSpace(request.PerformedByAdminId) + ? string.Empty + : $" (ادمین: {request.PerformedByAdminId})"; + + var transaction = new Transaction + { + Amount = request.Amount, + Description = $"شارژ دستی کیف پول اصلی - کاربر {user.Id}{notePart}{adminPart}", + PaymentStatus = PaymentStatus.Success, + PaymentDate = DateTime.Now, + RefId = refId, + Type = TransactionType.ManualCreditWalletCharge + }; + + _context.Transactions.Add(transaction); + await _context.SaveChangesAsync(cancellationToken); + + _context.UserWalletHistories.Add(new UserWalletHistory + { + WalletId = wallet.Id, + CurrentBalance = wallet.Balance, + ChangeValue = request.Amount, + CurrentNetworkBalance = wallet.NetworkBalance, + ChangeNerworkValue = 0, + CurrentDiscountBalance = wallet.DiscountBalance, + ChangeDiscountValue = 0, + IsIncrease = true, + RefrenceId = transaction.Id + }); + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Manual credit charge: UserId={UserId}, Amount={Amount}, OldBalance={Old}, NewBalance={New}, TxId={TxId}, Admin={Admin}", + user.Id, + request.Amount, + oldBalance, + wallet.Balance, + transaction.Id, + request.PerformedByAdminId); + + return new AdminManualCreditChargeResult + { + Success = true, + Message = "شارژ دستی کیف پول اصلی با موفقیت انجام شد", + TransactionId = transaction.Id, + NewBalance = wallet.Balance + }; + } +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Commands/AdminManualCreditCharge/AdminManualCreditChargeCommandValidator.cs b/src/CMSMicroservice.Application/WalletCQ/Commands/AdminManualCreditCharge/AdminManualCreditChargeCommandValidator.cs new file mode 100644 index 0000000..9b2c9f1 --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Commands/AdminManualCreditCharge/AdminManualCreditChargeCommandValidator.cs @@ -0,0 +1,20 @@ +using CMSMicroservice.Domain.Common; +using FluentValidation; + +namespace CMSMicroservice.Application.WalletCQ.Commands.AdminManualCreditCharge; + +public class AdminManualCreditChargeCommandValidator : AbstractValidator +{ + public AdminManualCreditChargeCommandValidator() + { + RuleFor(x => x.UserId) + .GreaterThan(0) + .WithMessage("شناسه کاربر باید بزرگتر از صفر باشد"); + + RuleFor(x => x.Amount) + .GreaterThanOrEqualTo(SystemConstants.DiscountWalletMinCharge) + .WithMessage("حداقل مبلغ شارژ ۱۰,۰۰۰ تومان است") + .LessThanOrEqualTo(SystemConstants.WalletMaxSafeAmount) + .WithMessage("مبلغ وارد شده بیش از حد مجاز است"); + } +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Queries/GetManualCreditCharges/GetManualCreditChargesQuery.cs b/src/CMSMicroservice.Application/WalletCQ/Queries/GetManualCreditCharges/GetManualCreditChargesQuery.cs new file mode 100644 index 0000000..ad30dfe --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Queries/GetManualCreditCharges/GetManualCreditChargesQuery.cs @@ -0,0 +1,39 @@ +using CMSMicroservice.Application.Common.Models; +using MediatR; + +namespace CMSMicroservice.Application.WalletCQ.Queries.GetManualCreditCharges; + +public class GetManualCreditChargesQuery : IRequest +{ + public PaginationState? PaginationState { get; set; } + public string? SortBy { get; set; } + public GetManualCreditChargesFilter? Filter { get; set; } +} + +public class GetManualCreditChargesFilter +{ + public long? UserId { get; set; } + public long? TransactionId { get; set; } + public string? RefId { get; set; } + public DateTime? CreatedFrom { get; set; } + public DateTime? CreatedTo { get; set; } +} + +public class GetManualCreditChargesResponseDto +{ + public MetaData MetaData { get; set; } = new(); + public List Models { get; set; } = new(); +} + +public class ManualCreditChargeModelDto +{ + public long TransactionId { get; set; } + public long UserId { get; set; } + public string UserName { get; set; } = string.Empty; + public long Amount { get; set; } + public string Description { get; set; } = string.Empty; + public string? RefId { get; set; } + public long NewBalance { get; set; } + public DateTime Created { get; set; } + public DateTime? PaymentDate { get; set; } +} diff --git a/src/CMSMicroservice.Application/WalletCQ/Queries/GetManualCreditCharges/GetManualCreditChargesQueryHandler.cs b/src/CMSMicroservice.Application/WalletCQ/Queries/GetManualCreditCharges/GetManualCreditChargesQueryHandler.cs new file mode 100644 index 0000000..1478eda --- /dev/null +++ b/src/CMSMicroservice.Application/WalletCQ/Queries/GetManualCreditCharges/GetManualCreditChargesQueryHandler.cs @@ -0,0 +1,69 @@ +using CMSMicroservice.Application.Common.Extensions; +using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace CMSMicroservice.Application.WalletCQ.Queries.GetManualCreditCharges; + +public class GetManualCreditChargesQueryHandler + : IRequestHandler +{ + private readonly IApplicationDbContext _context; + + public GetManualCreditChargesQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle( + GetManualCreditChargesQuery request, + CancellationToken cancellationToken) + { + var filter = request.Filter; + + var query = + from t in _context.Transactions.AsNoTracking() + where t.Type == TransactionType.ManualCreditWalletCharge + join h in _context.UserWalletHistories.AsNoTracking() on t.Id equals h.RefrenceId + join w in _context.UserWallets.AsNoTracking() on h.WalletId equals w.Id + join u in _context.Users.AsNoTracking() on w.UserId equals u.Id + select new ManualCreditChargeModelDto + { + TransactionId = t.Id, + UserId = w.UserId, + UserName = ((u.FirstName ?? "") + " " + (u.LastName ?? "")).Trim(), + Amount = t.Amount, + Description = t.Description ?? string.Empty, + RefId = t.RefId, + NewBalance = h.CurrentBalance, + Created = t.Created, + PaymentDate = t.PaymentDate + }; + + if (filter?.UserId is long userId) + query = query.Where(x => x.UserId == userId); + + if (filter?.TransactionId is long txId) + query = query.Where(x => x.TransactionId == txId); + + if (!string.IsNullOrWhiteSpace(filter?.RefId)) + query = query.Where(x => x.RefId != null && x.RefId.Contains(filter.RefId)); + + if (filter?.CreatedFrom is DateTime from) + query = query.Where(x => x.Created >= from); + + if (filter?.CreatedTo is DateTime to) + query = query.Where(x => x.Created <= to); + + query = query.OrderByDescending(x => x.Created); + + return new GetManualCreditChargesResponseDto + { + MetaData = await query.GetMetaData(request.PaginationState, cancellationToken), + Models = await query + .PaginatedListAsync(paginationState: request.PaginationState) + .ToListAsync(cancellationToken) + }; + } +} diff --git a/src/CMSMicroservice.Domain/Enums/TransactionType.cs b/src/CMSMicroservice.Domain/Enums/TransactionType.cs index 7b32d15..51564ad 100644 --- a/src/CMSMicroservice.Domain/Enums/TransactionType.cs +++ b/src/CMSMicroservice.Domain/Enums/TransactionType.cs @@ -41,4 +41,9 @@ public enum TransactionType /// شارژ کیف پول اصلی از درگاه /// CreditWalletCharge = 16, + + /// + /// شارژ دستی کیف پول اصلی توسط ادمین + /// + ManualCreditWalletCharge = 17, } diff --git a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj index cc8bf9d..d205872 100644 --- a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj +++ b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj @@ -3,7 +3,7 @@ net9.0 enable enable - 0.0.204 + 0.0.205 None False False diff --git a/src/CMSMicroservice.Protobuf/Protos/public_messages.proto b/src/CMSMicroservice.Protobuf/Protos/public_messages.proto index a3c55c6..99fd260 100644 --- a/src/CMSMicroservice.Protobuf/Protos/public_messages.proto +++ b/src/CMSMicroservice.Protobuf/Protos/public_messages.proto @@ -116,6 +116,7 @@ enum TransactionType MagicWalletDeposit = 14; MagicWalletBonus = 15; CreditWalletCharge = 16; + ManualCreditWalletCharge = 17; } enum ContractType { diff --git a/src/CMSMicroservice.Protobuf/Protos/userwallet.proto b/src/CMSMicroservice.Protobuf/Protos/userwallet.proto index ab9a1a8..9dd87a8 100644 --- a/src/CMSMicroservice.Protobuf/Protos/userwallet.proto +++ b/src/CMSMicroservice.Protobuf/Protos/userwallet.proto @@ -125,6 +125,20 @@ service UserWalletContract body: "*" }; }; + + // ============= Admin Manual Credit Charge ============= + + rpc AdminManualCreditCharge(AdminManualCreditChargeRequest) returns (AdminManualCreditChargeResponse){ + option (google.api.http) = { + post: "/Admin/ManualCreditCharge" + body: "*" + }; + }; + rpc GetManualCreditCharges(GetManualCreditChargesRequest) returns (GetManualCreditChargesResponse){ + option (google.api.http) = { + get: "/Admin/GetManualCreditCharges" + }; + }; } message CreateNewUserWalletRequest { @@ -324,4 +338,56 @@ message VerifyWalletChargeResponse { bool success = 1; string message = 2; +} + +// ============= Admin Manual Credit Charge Messages ============= + +message AdminManualCreditChargeRequest +{ + int64 user_id = 1; + int64 amount = 2; // تومان + google.protobuf.StringValue note = 3; +} + +message AdminManualCreditChargeResponse +{ + bool success = 1; + string message = 2; + int64 transaction_id = 3; + int64 new_balance = 4; +} + +message GetManualCreditChargesRequest +{ + messages.PaginationState pagination_state = 1; + google.protobuf.StringValue sort_by = 2; + GetManualCreditChargesFilter filter = 3; +} + +message GetManualCreditChargesFilter +{ + google.protobuf.Int64Value user_id = 1; + google.protobuf.Int64Value transaction_id = 2; + google.protobuf.StringValue ref_id = 3; + google.protobuf.Timestamp created_from = 4; + google.protobuf.Timestamp created_to = 5; +} + +message GetManualCreditChargesResponse +{ + messages.MetaData meta_data = 1; + repeated ManualCreditChargeModel models = 2; +} + +message ManualCreditChargeModel +{ + int64 transaction_id = 1; + int64 user_id = 2; + string user_name = 3; + int64 amount = 4; + string description = 5; + string ref_id = 6; + int64 new_balance = 7; + google.protobuf.Timestamp created = 8; + google.protobuf.Timestamp payment_date = 9; } \ No newline at end of file diff --git a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs index 72338f0..e8b8ff4 100644 --- a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs +++ b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs @@ -9,6 +9,9 @@ using CMSMicroservice.Application.WalletCQ.Commands.ChargeCreditWallet; using CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge; using CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge; using CMSMicroservice.Application.WalletCQ.Commands.VerifyCreditWalletCharge; +using CMSMicroservice.Application.WalletCQ.Commands.AdminManualCreditCharge; +using CMSMicroservice.Application.WalletCQ.Queries.GetManualCreditCharges; +using AppManualChargeFilter = CMSMicroservice.Application.WalletCQ.Queries.GetManualCreditCharges.GetManualCreditChargesFilter; using CMSMicroservice.Application.UserWalletCQ.Queries.GetUserWallet; using CMSMicroservice.Application.UserWalletCQ.Queries.GetAllUserWalletByFilter; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory; @@ -518,4 +521,116 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase MinWithdrawalAmount = settings.MinWithdrawalAmount }; } + + // ============= Admin Manual Credit Charge ============= + + public override async Task AdminManualCreditCharge( + AdminManualCreditChargeRequest request, ServerCallContext context) + { + try + { + var result = await _sender.Send(new AdminManualCreditChargeCommand + { + UserId = request.UserId, + Amount = request.Amount, + Note = request.Note, + PerformedByAdminId = _currentUserService.GetPerformedBy() + }, context.CancellationToken); + + return new AdminManualCreditChargeResponse + { + Success = result.Success, + Message = result.Message ?? string.Empty, + TransactionId = result.TransactionId, + NewBalance = result.NewBalance + }; + } + catch (PaymentInProgressException ex) + { + return new AdminManualCreditChargeResponse + { + Success = false, + Message = ex.Message + }; + } + catch (NotFoundException ex) + { + return new AdminManualCreditChargeResponse + { + Success = false, + Message = ex.Message + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "AdminManualCreditCharge failed for UserId={UserId}", request.UserId); + return new AdminManualCreditChargeResponse + { + Success = false, + Message = ex.Message + }; + } + } + + public override async Task GetManualCreditCharges( + GetManualCreditChargesRequest request, ServerCallContext context) + { + var result = await _sender.Send(new GetManualCreditChargesQuery + { + SortBy = request.SortBy, + PaginationState = request.PaginationState != null + ? new Application.Common.Models.PaginationState + { + PageNumber = request.PaginationState.PageNumber, + PageSize = request.PaginationState.PageSize + } + : new Application.Common.Models.PaginationState { PageNumber = 1, PageSize = 20 }, + Filter = new AppManualChargeFilter + { + UserId = request.Filter?.UserId, + TransactionId = request.Filter?.TransactionId, + RefId = request.Filter?.RefId, + CreatedFrom = request.Filter?.CreatedFrom?.ToDateTime(), + CreatedTo = request.Filter?.CreatedTo?.ToDateTime() + } + }, context.CancellationToken); + + var response = new GetManualCreditChargesResponse + { + MetaData = new CMSMicroservice.Protobuf.Protos.MetaData + { + CurrentPage = result.MetaData.CurrentPage, + TotalPage = result.MetaData.TotalPage, + PageSize = result.MetaData.PageSize, + TotalCount = result.MetaData.TotalCount, + HasPrevious = result.MetaData.HasPrevious, + HasNext = result.MetaData.HasNext + } + }; + + foreach (var m in result.Models) + { + var model = new ManualCreditChargeModel + { + TransactionId = m.TransactionId, + UserId = m.UserId, + UserName = m.UserName ?? string.Empty, + Amount = m.Amount, + Description = m.Description ?? string.Empty, + RefId = m.RefId ?? string.Empty, + NewBalance = m.NewBalance, + Created = Timestamp.FromDateTime(DateTime.SpecifyKind(m.Created, DateTimeKind.Utc)) + }; + + if (m.PaymentDate.HasValue) + { + model.PaymentDate = Timestamp.FromDateTime( + DateTime.SpecifyKind(m.PaymentDate.Value, DateTimeKind.Utc)); + } + + response.Models.Add(model); + } + + return response; + } }