feat(wallet): add GetMagicWalletCharges RPC and related data structures
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 16m57s

- Introduced GetMagicWalletCharges RPC in userwallet.proto for fetching magic wallet charges.
- Added request and response message definitions for GetMagicWalletCharges, including pagination and filtering options.
- Updated UserWalletService to implement the new RPC, handling request parameters and constructing the response with relevant data.
- Bumped project version to 0.0.215 to reflect the new feature addition.
This commit is contained in:
masoodafar-web
2026-10-13 01:28:04 +03:30
parent e7eb50ec35
commit ba3a928bd0
5 changed files with 326 additions and 1 deletions
@@ -0,0 +1,41 @@
using CMSMicroservice.Application.Common.Models;
using MediatR;
namespace CMSMicroservice.Application.WalletCQ.Queries.GetMagicWalletCharges;
public class GetMagicWalletChargesQuery : IRequest<GetMagicWalletChargesResponseDto>
{
public PaginationState? PaginationState { get; set; }
public string? SortBy { get; set; }
public GetMagicWalletChargesFilter? Filter { get; set; }
}
public class GetMagicWalletChargesFilter
{
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 GetMagicWalletChargesResponseDto
{
public MetaData MetaData { get; set; } = new();
public List<MagicWalletChargeModelDto> Models { get; set; } = new();
}
public class MagicWalletChargeModelDto
{
public long TransactionId { get; set; }
public long UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public long DepositAmount { get; set; }
public double Multiplier { get; set; }
public long CreditedAmount { get; set; }
public long? RemainingCapacity { get; set; }
public string? RefId { get; set; }
public long NewBalance { get; set; }
public DateTime Created { get; set; }
public DateTime? PaymentDate { get; set; }
}
@@ -0,0 +1,171 @@
using CMSMicroservice.Application.Common.Extensions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.WalletCQ.Queries.GetMagicWalletCharges;
public class GetMagicWalletChargesQueryHandler
: IRequestHandler<GetMagicWalletChargesQuery, GetMagicWalletChargesResponseDto>
{
private readonly IApplicationDbContext _context;
public GetMagicWalletChargesQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetMagicWalletChargesResponseDto> Handle(
GetMagicWalletChargesQuery request,
CancellationToken cancellationToken)
{
var filter = request.Filter;
var query =
from t in _context.Transactions.AsNoTracking()
where t.Type == TransactionType.MagicWalletDeposit
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 MagicWalletChargeModelDto
{
TransactionId = t.Id,
UserId = w.UserId,
UserName = ((u.FirstName ?? "") + " " + (u.LastName ?? "")).Trim(),
DepositAmount = t.Amount,
CreditedAmount = h.ChangeValue,
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);
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
var models = await query
.PaginatedListAsync(paginationState: request.PaginationState)
.ToListAsync(cancellationToken);
foreach (var model in models)
{
model.Multiplier = model.DepositAmount == 0
? 0
: (double)model.CreditedAmount / model.DepositAmount;
}
await PopulateRemainingCapacityAsync(models, cancellationToken);
return new GetMagicWalletChargesResponseDto
{
MetaData = meta,
Models = models
};
}
private async Task PopulateRemainingCapacityAsync(
List<MagicWalletChargeModelDto> models,
CancellationToken cancellationToken)
{
if (models.Count == 0)
return;
var userIds = models.Select(m => m.UserId).Distinct().ToList();
var wallets = await _context.UserWallets.AsNoTracking()
.Where(w => userIds.Contains(w.UserId))
.Select(w => new { w.UserId, w.MagicActivatedAt })
.ToListAsync(cancellationToken);
var walletByUser = wallets.ToDictionary(w => w.UserId, w => w.MagicActivatedAt);
var cycles = await _context.ClubMembershipCycles.AsNoTracking()
.Where(c => userIds.Contains(c.UserId) && c.IsCurrentCycle)
.Select(c => new { c.UserId, c.PackageId })
.ToListAsync(cancellationToken);
var packageIds = cycles.Select(c => c.PackageId).Distinct().ToList();
var packages = await _context.Packages.AsNoTracking()
.Where(p => packageIds.Contains(p.Id) || (p.IsBasePackage && !p.IsDeleted))
.Select(p => new { p.Id, p.IsBasePackage, p.IsDeleted, p.MagicWalletMaxDeposit })
.ToListAsync(cancellationToken);
var basePackageMax = packages
.Where(p => p.IsBasePackage && !p.IsDeleted)
.Select(p => (long?)p.MagicWalletMaxDeposit)
.FirstOrDefault();
var packageIdByUser = cycles.ToDictionary(c => c.UserId, c => c.PackageId);
var maxByPackageId = packages.ToDictionary(p => p.Id, p => p.MagicWalletMaxDeposit);
// All magic deposits for these users (for cumulative sum within magic cycle)
var depositRows = await (
from t in _context.Transactions.AsNoTracking()
where t.Type == TransactionType.MagicWalletDeposit
join h in _context.UserWalletHistories.AsNoTracking() on t.Id equals h.RefrenceId
join w in _context.UserWallets.AsNoTracking() on h.WalletId equals w.Id
where userIds.Contains(w.UserId)
select new { w.UserId, t.Id, t.Amount, t.Created }
).ToListAsync(cancellationToken);
var depositsByUser = depositRows
.GroupBy(d => d.UserId)
.ToDictionary(g => g.Key, g => g.OrderBy(x => x.Created).ThenBy(x => x.Id).ToList());
foreach (var model in models)
{
if (!walletByUser.TryGetValue(model.UserId, out var magicActivatedAt) || !magicActivatedAt.HasValue)
{
model.RemainingCapacity = null;
continue;
}
long? maxDeposit = null;
if (packageIdByUser.TryGetValue(model.UserId, out var pkgId) &&
maxByPackageId.TryGetValue(pkgId, out var pkgMax))
{
maxDeposit = pkgMax;
}
else
{
maxDeposit = basePackageMax;
}
if (maxDeposit is null)
{
model.RemainingCapacity = null;
continue;
}
if (!depositsByUser.TryGetValue(model.UserId, out var userDeposits))
{
model.RemainingCapacity = maxDeposit;
continue;
}
var depositedThroughThis = userDeposits
.Where(d => d.Created >= magicActivatedAt.Value &&
(d.Created < model.Created ||
(d.Created == model.Created && d.Id <= model.TransactionId)))
.Sum(d => d.Amount);
model.RemainingCapacity = Math.Max(0, maxDeposit.Value - depositedThroughThis);
}
}
}
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>0.0.214</Version> <Version>0.0.215</Version>
<DebugType>None</DebugType> <DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols> <DebugSymbols>False</DebugSymbols>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild> <GeneratePackageOnBuild>False</GeneratePackageOnBuild>
@@ -139,6 +139,14 @@ service UserWalletContract
get: "/Admin/GetManualCreditCharges" get: "/Admin/GetManualCreditCharges"
}; };
}; };
// ============= Admin Magic Wallet Charges =============
rpc GetMagicWalletCharges(GetMagicWalletChargesRequest) returns (GetMagicWalletChargesResponse){
option (google.api.http) = {
get: "/Admin/GetMagicWalletCharges"
};
};
} }
message CreateNewUserWalletRequest message CreateNewUserWalletRequest
{ {
@@ -391,3 +399,40 @@ message ManualCreditChargeModel
google.protobuf.Timestamp created = 8; google.protobuf.Timestamp created = 8;
google.protobuf.Timestamp payment_date = 9; google.protobuf.Timestamp payment_date = 9;
} }
message GetMagicWalletChargesRequest
{
messages.PaginationState pagination_state = 1;
google.protobuf.StringValue sort_by = 2;
GetMagicWalletChargesFilter filter = 3;
}
message GetMagicWalletChargesFilter
{
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 GetMagicWalletChargesResponse
{
messages.MetaData meta_data = 1;
repeated MagicWalletChargeModel models = 2;
}
message MagicWalletChargeModel
{
int64 transaction_id = 1;
int64 user_id = 2;
string user_name = 3;
int64 deposit_amount = 4;
double multiplier = 5;
int64 credited_amount = 6;
google.protobuf.Int64Value remaining_capacity = 7;
string ref_id = 8;
int64 new_balance = 9;
google.protobuf.Timestamp created = 10;
google.protobuf.Timestamp payment_date = 11;
}
@@ -12,6 +12,8 @@ using CMSMicroservice.Application.WalletCQ.Commands.VerifyCreditWalletCharge;
using CMSMicroservice.Application.WalletCQ.Commands.AdminManualCreditCharge; using CMSMicroservice.Application.WalletCQ.Commands.AdminManualCreditCharge;
using CMSMicroservice.Application.WalletCQ.Queries.GetManualCreditCharges; using CMSMicroservice.Application.WalletCQ.Queries.GetManualCreditCharges;
using AppManualChargeFilter = CMSMicroservice.Application.WalletCQ.Queries.GetManualCreditCharges.GetManualCreditChargesFilter; using AppManualChargeFilter = CMSMicroservice.Application.WalletCQ.Queries.GetManualCreditCharges.GetManualCreditChargesFilter;
using CMSMicroservice.Application.WalletCQ.Queries.GetMagicWalletCharges;
using AppMagicChargeFilter = CMSMicroservice.Application.WalletCQ.Queries.GetMagicWalletCharges.GetMagicWalletChargesFilter;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetUserWallet; using CMSMicroservice.Application.UserWalletCQ.Queries.GetUserWallet;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetAllUserWalletByFilter; using CMSMicroservice.Application.UserWalletCQ.Queries.GetAllUserWalletByFilter;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory;
@@ -653,4 +655,70 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
return response; return response;
} }
public override async Task<GetMagicWalletChargesResponse> GetMagicWalletCharges(
GetMagicWalletChargesRequest request, ServerCallContext context)
{
var result = await _sender.Send(new GetMagicWalletChargesQuery
{
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 AppMagicChargeFilter
{
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 GetMagicWalletChargesResponse
{
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 MagicWalletChargeModel
{
TransactionId = m.TransactionId,
UserId = m.UserId,
UserName = m.UserName ?? string.Empty,
DepositAmount = m.DepositAmount,
Multiplier = m.Multiplier,
CreditedAmount = m.CreditedAmount,
RefId = m.RefId ?? string.Empty,
NewBalance = m.NewBalance,
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(m.Created, DateTimeKind.Utc))
};
if (m.RemainingCapacity.HasValue)
model.RemainingCapacity = m.RemainingCapacity.Value;
if (m.PaymentDate.HasValue)
{
model.PaymentDate = Timestamp.FromDateTime(
DateTime.SpecifyKind(m.PaymentDate.Value, DateTimeKind.Utc));
}
response.Models.Add(model);
}
return response;
}
} }