From f79affb44d308649bc0d6d36a022310f2bdeb5c0 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Thu, 24 Sep 2026 19:28:01 +0330 Subject: [PATCH] feat(transactions): enhance transaction queries with user information - Added UserFullName and UserId properties to transaction response models for better user context. - Updated query handlers to populate user information from related entities, ensuring accurate user details in transaction responses. - Modified Protobuf definitions to include new fields for user identification and display. - Adjusted mapping profiles to accommodate new user-related fields in transaction data. --- scripts/usp_CleanupTestUser.sql | 340 ++++++++++++++++++ .../GetAllPaymentTransactionByFilterQuery.cs | 1 + ...lPaymentTransactionByFilterQueryHandler.cs | 55 +-- .../GetAllTransactionsByFilterQueryHandler.cs | 113 +++++- .../GetAllTransactionsByFilterResponseDto.cs | 3 + .../CMSMicroservice.Protobuf.csproj | 2 +- .../Protos/paymenttransaction.proto | 1 + .../Protos/transactions.proto | 3 + .../Mappings/PaymentTransactionProfile.cs | 3 +- .../Common/Mappings/TransactionsProfile.cs | 5 +- 10 files changed, 488 insertions(+), 38 deletions(-) create mode 100644 scripts/usp_CleanupTestUser.sql diff --git a/scripts/usp_CleanupTestUser.sql b/scripts/usp_CleanupTestUser.sql new file mode 100644 index 0000000..a2ee7de --- /dev/null +++ b/scripts/usp_CleanupTestUser.sql @@ -0,0 +1,340 @@ +/* + Cleanup all side-effects of a test FrontOffice user (register + package + club + wallet + invitations). + + Usage: + EXEC dbo.usp_CleanupTestUser @Mobile = N'09037437157', @DryRun = 1; + EXEC dbo.usp_CleanupTestUser @Mobile = N'09037437157'; + EXEC dbo.usp_CleanupTestUser @Mobile = N'09...', @ReparentChildren = 1; + EXEC dbo.usp_CleanupTestUser @Mobile = N'09...', @ReversePool = 1, @AbortIfPoolCalculated = 1; + + Pool reverse: for each ClubMembershipCycle, subtract Packages.ActivationFee + (fallback: ClubMembership.InitialContribution) from CMS.WeeklyCommissionPools + for matching WeekDefinition × PackageId. + + Does NOT delete pool rows (shared). Does NOT claw back commission already paid to OTHER users. + Does NOT touch: Packages master, Contracts master, WeekDefinitions, Roles, catalogs. +*/ +CREATE OR ALTER PROCEDURE dbo.usp_CleanupTestUser + @UserId BIGINT = NULL, + @Mobile NVARCHAR(20) = NULL, + @HardDeleteUser BIT = 1, + @ReparentChildren BIT = 0, + @ReversePool BIT = 1, + @AbortIfPoolCalculated BIT = 0, + @DryRun BIT = 0 +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + SET QUOTED_IDENTIFIER ON; + SET ANSI_NULLS ON; + + IF @UserId IS NULL AND (@Mobile IS NULL OR LTRIM(RTRIM(@Mobile)) = N'') + BEGIN + RAISERROR(N'Provide @UserId and/or @Mobile.', 16, 1); + RETURN; + END; + + DECLARE @MobileNorm NVARCHAR(20) = NULL; + IF @Mobile IS NOT NULL + BEGIN + SET @MobileNorm = LTRIM(RTRIM(@Mobile)); + IF LEFT(@MobileNorm, 3) = N'+98' SET @MobileNorm = N'0' + SUBSTRING(@MobileNorm, 4, 20); + IF LEFT(@MobileNorm, 2) = N'98' AND LEN(@MobileNorm) = 12 SET @MobileNorm = N'0' + SUBSTRING(@MobileNorm, 3, 20); + IF LEFT(@MobileNorm, 1) = N'9' AND LEN(@MobileNorm) = 10 SET @MobileNorm = N'0' + @MobileNorm; + END; + + IF @UserId IS NULL + BEGIN + SELECT TOP (1) @UserId = Id + FROM CMS.Users + WHERE Mobile = @MobileNorm + OR Mobile = @Mobile + OR REPLACE(Mobile, N'+98', N'0') = @MobileNorm + ORDER BY IsDeleted ASC, Id DESC; + END; + + IF @UserId IS NULL + BEGIN + RAISERROR(N'User not found for given @UserId/@Mobile.', 16, 1); + RETURN; + END; + + DECLARE @UserMobile NVARCHAR(20); + SELECT @UserMobile = Mobile FROM CMS.Users WHERE Id = @UserId; + IF @UserMobile IS NULL + BEGIN + RAISERROR(N'UserId %I64d not found.', 16, 1, @UserId); + RETURN; + END; + + DECLARE @ChildCount INT = + (SELECT COUNT(*) FROM CMS.Users WHERE NetworkParentId = @UserId AND IsDeleted = 0); + + IF @ChildCount > 0 AND @ReparentChildren = 0 AND @HardDeleteUser = 1 + BEGIN + RAISERROR(N'User has %d active network children. Pass @ReparentChildren=1 or soft-delete only.', 16, 1, @ChildCount); + RETURN; + END; + + PRINT N'=== usp_CleanupTestUser ==='; + PRINT N'UserId=' + CAST(@UserId AS NVARCHAR(20)) + N' Mobile=' + ISNULL(@UserMobile, N'?') + + N' DryRun=' + CAST(@DryRun AS NVARCHAR(1)) + + N' ReversePool=' + CAST(@ReversePool AS NVARCHAR(1)); + + DECLARE @Wallets TABLE (WalletId BIGINT PRIMARY KEY); + INSERT INTO @Wallets (WalletId) + SELECT Id FROM CMS.UserWallets WHERE UserId = @UserId; + + DECLARE @Tx TABLE (TransactionId BIGINT PRIMARY KEY); + INSERT INTO @Tx (TransactionId) + SELECT DISTINCT x.TransactionId + FROM ( + SELECT TransactionId FROM CMS.UserPackagePurchases WHERE UserId = @UserId AND TransactionId IS NOT NULL + UNION + SELECT TransactionId FROM CMS.PaymentTransactions WHERE UserId = @UserId AND TransactionId IS NOT NULL + UNION + SELECT TransactionId FROM CMS.UserOrders WHERE UserId = @UserId AND TransactionId IS NOT NULL + UNION + SELECT TransactionId FROM CMS.ManualPayments WHERE UserId = @UserId AND TransactionId IS NOT NULL + UNION + SELECT TransactionId FROM CMS.DayaLoanContracts WHERE UserId = @UserId AND TransactionId IS NOT NULL + UNION + SELECT TransactionId FROM CMS.DiscountOrders WHERE UserId = @UserId AND TransactionId IS NOT NULL + UNION + SELECT h.RefrenceId + FROM CMS.UserWalletHistories h + INNER JOIN @Wallets w ON w.WalletId = h.WalletId + WHERE h.RefrenceId IS NOT NULL + ) x + WHERE x.TransactionId IS NOT NULL; + + DECLARE @Orders TABLE (OrderId BIGINT PRIMARY KEY); + INSERT INTO @Orders (OrderId) + SELECT Id FROM CMS.UserOrders WHERE UserId = @UserId; + + DECLARE @DiscountOrders TABLE (DiscountOrderId BIGINT PRIMARY KEY); + INSERT INTO @DiscountOrders (DiscountOrderId) + SELECT Id FROM CMS.DiscountOrders WHERE UserId = @UserId; + + DECLARE @ClubMemberships TABLE (ClubMembershipId BIGINT PRIMARY KEY); + INSERT INTO @ClubMemberships (ClubMembershipId) + SELECT Id FROM CMS.ClubMemberships WHERE UserId = @UserId; + + DECLARE @Cycles TABLE (CycleId BIGINT PRIMARY KEY); + INSERT INTO @Cycles (CycleId) + SELECT Id FROM CMS.ClubMembershipCycles WHERE UserId = @UserId + UNION + SELECT Id FROM CMS.ClubMembershipCycles WHERE ClubMembershipId IN (SELECT ClubMembershipId FROM @ClubMemberships); + + DECLARE @Payouts TABLE (PayoutId BIGINT PRIMARY KEY); + INSERT INTO @Payouts (PayoutId) + SELECT Id FROM CMS.UserCommissionPayouts WHERE UserId = @UserId; + + DECLARE @PoolReversals TABLE ( + CycleId BIGINT, + PackageId BIGINT, + WeekDefinitionId BIGINT NULL, + PoolId BIGINT NULL, + Fee BIGINT, + PoolAmountBefore BIGINT NULL, + IsCalculated BIT NULL, + Note NVARCHAR(200) + ); + + INSERT INTO @PoolReversals (CycleId, PackageId, WeekDefinitionId, PoolId, Fee, PoolAmountBefore, IsCalculated, Note) + SELECT + c.Id, + c.PackageId, + wd.Id, + wp.Id, + ISNULL(NULLIF(cm.InitialContribution, 0), ISNULL(p.ActivationFee, 0)), + wp.TotalPoolAmount, + wp.IsCalculated, + CASE + WHEN wd.Id IS NULL THEN N'NO_WEEK_MATCH' + WHEN wp.Id IS NULL THEN N'NO_POOL_ROW' + WHEN wp.IsCalculated = 1 THEN N'POOL_ALREADY_CALCULATED' + ELSE N'OK' + END + FROM CMS.ClubMembershipCycles c + LEFT JOIN CMS.ClubMemberships cm ON cm.Id = c.ClubMembershipId + LEFT JOIN CMS.Packages p ON p.Id = c.PackageId + LEFT JOIN CMS.WeekDefinitions wd + ON CAST(c.PackagePurchasedAt AS DATE) >= CAST(wd.StartDate AS DATE) + AND CAST(c.PackagePurchasedAt AS DATE) <= CAST(wd.EndDate AS DATE) + AND wd.IsDeleted = 0 + LEFT JOIN CMS.WeeklyCommissionPools wp + ON wp.WeekDefinitionId = wd.Id + AND wp.PackageId = c.PackageId + AND wp.IsDeleted = 0 + WHERE c.Id IN (SELECT CycleId FROM @Cycles); + + SELECT N'Users' AS Entity, COUNT(*) AS Cnt FROM CMS.Users WHERE Id = @UserId + UNION ALL SELECT N'OtpTokens', COUNT(*) FROM CMS.OtpTokens WHERE Mobile IN (@UserMobile, @MobileNorm, @Mobile) + UNION ALL SELECT N'UserInvitations', COUNT(*) FROM CMS.UserInvitations WHERE InviterUserId = @UserId OR InviteeUserId = @UserId + UNION ALL SELECT N'ClubMemberships', COUNT(*) FROM @ClubMemberships + UNION ALL SELECT N'ClubMembershipCycles', COUNT(*) FROM @Cycles + UNION ALL SELECT N'UserPackagePurchases', COUNT(*) FROM CMS.UserPackagePurchases WHERE UserId = @UserId + UNION ALL SELECT N'PaymentTransactions', COUNT(*) FROM CMS.PaymentTransactions WHERE UserId = @UserId OR Mobile = @UserMobile + UNION ALL SELECT N'UserWallets', COUNT(*) FROM @Wallets + UNION ALL SELECT N'UserWalletHistories', COUNT(*) FROM CMS.UserWalletHistories WHERE WalletId IN (SELECT WalletId FROM @Wallets) + UNION ALL SELECT N'Transactions(collected)', COUNT(*) FROM @Tx + UNION ALL SELECT N'NetworkChildren', @ChildCount + UNION ALL SELECT N'UserOrders', COUNT(*) FROM @Orders + UNION ALL SELECT N'DiscountOrders', COUNT(*) FROM @DiscountOrders + UNION ALL SELECT N'UserCommissionPayouts', COUNT(*) FROM @Payouts + UNION ALL SELECT N'PoolReversals(planned)', COUNT(*) FROM @PoolReversals WHERE Fee > 0; + + SELECT N'--- Pool reverse plan ---' AS Info; + SELECT * FROM @PoolReversals; + + IF @ReversePool = 1 AND @AbortIfPoolCalculated = 1 + AND EXISTS (SELECT 1 FROM @PoolReversals WHERE IsCalculated = 1 AND Fee > 0) + BEGIN + RAISERROR(N'One or more target weekly pools are already calculated. Aborting (@AbortIfPoolCalculated=1).', 16, 1); + RETURN; + END; + + IF @DryRun = 1 + BEGIN + PRINT N'DryRun=1 — no changes committed.'; + RETURN; + END; + + BEGIN TRANSACTION; + + BEGIN TRY + /* ---- Reverse weekly pool charges FIRST (needs cycle + package data) ---- */ + IF @ReversePool = 1 + BEGIN + UPDATE wp + SET wp.TotalPoolAmount = CASE + WHEN wp.TotalPoolAmount >= r.Fee THEN wp.TotalPoolAmount - r.Fee + ELSE 0 + END, + wp.LastModified = SYSUTCDATETIME() + FROM CMS.WeeklyCommissionPools wp + INNER JOIN ( + SELECT PoolId, SUM(Fee) AS Fee + FROM @PoolReversals + WHERE PoolId IS NOT NULL AND Fee > 0 + GROUP BY PoolId + ) r ON r.PoolId = wp.Id; + + PRINT N'Pool reverse applied for ' + CAST(@@ROWCOUNT AS NVARCHAR(20)) + N' pool row(s).'; + END; + + /* ---- Club ---- */ + DELETE h FROM CMS.ClubMembershipCycleHistories h + WHERE h.UserId = @UserId OR h.ClubMembershipCycleId IN (SELECT CycleId FROM @Cycles); + + DELETE FROM CMS.ClubMembershipCycles + WHERE Id IN (SELECT CycleId FROM @Cycles) OR UserId = @UserId; + + DELETE FROM CMS.ClubMembershipHistories WHERE UserId = @UserId + OR ClubMembershipId IN (SELECT ClubMembershipId FROM @ClubMemberships); + + DELETE FROM CMS.UserClubFeatures WHERE UserId = @UserId + OR ClubMembershipId IN (SELECT ClubMembershipId FROM @ClubMemberships); + + DELETE FROM CMS.ClubMemberships WHERE UserId = @UserId; + + /* ---- Commission payouts of THIS user only ---- */ + DELETE h FROM CMS.CommissionPayoutHistories h + WHERE h.UserId = @UserId OR h.UserCommissionPayoutId IN (SELECT PayoutId FROM @Payouts); + + DELETE FROM CMS.UserCommissionPayouts WHERE UserId = @UserId; + + /* ---- Network ---- */ + DELETE FROM CMS.NetworkWeeklyBalances WHERE UserId = @UserId; + DELETE FROM CMS.NetworkMembershipHistories + WHERE UserId = @UserId OR OldParentId = @UserId OR NewParentId = @UserId; + + IF @ChildCount > 0 AND @ReparentChildren = 1 + UPDATE CMS.Users SET NetworkParentId = NULL, LastModified = SYSUTCDATETIME() + WHERE NetworkParentId = @UserId; + + /* ---- Invitations ---- */ + DELETE FROM CMS.UserInvitations + WHERE InviterUserId = @UserId OR InviteeUserId = @UserId; + + /* ---- Package / payment ---- */ + DELETE FROM CMS.UserPackagePurchases WHERE UserId = @UserId; + DELETE FROM CMS.PaymentTransactions + WHERE UserId = @UserId OR Mobile = @UserMobile OR Mobile = @MobileNorm; + + /* ---- Manual / Daya ---- */ + DELETE FROM CMS.ManualPayments WHERE UserId = @UserId; + DELETE FROM CMS.DayaLoanContracts WHERE UserId = @UserId; + + /* ---- Discount shop ---- */ + DELETE d FROM CMS.DiscountOrderDetails d + WHERE d.DiscountOrderId IN (SELECT DiscountOrderId FROM @DiscountOrders); + DELETE FROM CMS.DiscountOrders WHERE UserId = @UserId; + DELETE FROM CMS.DiscountShoppingCarts WHERE UserId = @UserId; + + /* ---- Stock ---- */ + DELETE FROM CMS.StockMovements + WHERE PerformedByUserId = @UserId + OR OrderId IN (SELECT OrderId FROM @Orders) + OR DiscountOrderId IN (SELECT DiscountOrderId FROM @DiscountOrders); + + /* ---- Orders ---- */ + DELETE FROM CMS.FactorDetails WHERE OrderId IN (SELECT OrderId FROM @Orders); + DELETE FROM CMS.OrderVATs WHERE OrderId IN (SELECT OrderId FROM @Orders); + DELETE FROM CMS.UserOrders WHERE UserId = @UserId; + + /* ---- Wallet ---- */ + DELETE FROM CMS.UserWalletHistories WHERE WalletId IN (SELECT WalletId FROM @Wallets); + DELETE FROM CMS.UserWallets WHERE UserId = @UserId; + + /* ---- Cart / address / roles / contracts ---- */ + DELETE FROM CMS.UserCarts WHERE UserId = @UserId; + DELETE FROM CMS.UserAddresses WHERE UserId = @UserId; + DELETE FROM CMS.UserRoles WHERE UserId = @UserId; + DELETE FROM CMS.UserContracts WHERE UserId = @UserId; + + /* ---- Content authored by test user ---- */ + UPDATE CMS.BlogPosts SET IsDeleted = 1, LastModified = SYSUTCDATETIME() + WHERE AuthorUserId = @UserId AND IsDeleted = 0; + + UPDATE CMS.PublicMessages SET IsDeleted = 1, LastModified = SYSUTCDATETIME() + WHERE CreatedByUserId = @UserId AND IsDeleted = 0; + + /* ---- Transactions ---- */ + DELETE t + FROM CMS.Transactions t + WHERE t.Id IN (SELECT TransactionId FROM @Tx) + AND NOT EXISTS (SELECT 1 FROM CMS.UserPackagePurchases p WHERE p.TransactionId = t.Id) + AND NOT EXISTS (SELECT 1 FROM CMS.PaymentTransactions p WHERE p.TransactionId = t.Id) + AND NOT EXISTS (SELECT 1 FROM CMS.UserOrders o WHERE o.TransactionId = t.Id) + AND NOT EXISTS (SELECT 1 FROM CMS.ManualPayments m WHERE m.TransactionId = t.Id) + AND NOT EXISTS (SELECT 1 FROM CMS.DayaLoanContracts d WHERE d.TransactionId = t.Id) + AND NOT EXISTS (SELECT 1 FROM CMS.DiscountOrders d WHERE d.TransactionId = t.Id); + + /* ---- OTP ---- */ + DELETE FROM CMS.OtpTokens + WHERE Mobile IN (@UserMobile, @MobileNorm, ISNULL(@Mobile, N'')); + + /* ---- User ---- */ + IF @HardDeleteUser = 1 + DELETE FROM CMS.Users WHERE Id = @UserId; + ELSE + UPDATE CMS.Users + SET IsDeleted = 1, LastModified = SYSUTCDATETIME() + WHERE Id = @UserId; + + COMMIT TRANSACTION; + + PRINT N'Cleanup completed successfully.'; + SELECT N'DONE' AS Status, @UserId AS UserId, @UserMobile AS Mobile, + @HardDeleteUser AS HardDeleted, @ReversePool AS PoolReversed; + END TRY + BEGIN CATCH + IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; + DECLARE @Err NVARCHAR(4000) = ERROR_MESSAGE(); + RAISERROR(N'Cleanup failed: %s', 16, 1, @Err); + END CATCH +END; +GO diff --git a/src/CMSMicroservice.Application/PaymentTransactionCQ/Queries/GetAllPaymentTransactionByFilter/GetAllPaymentTransactionByFilterQuery.cs b/src/CMSMicroservice.Application/PaymentTransactionCQ/Queries/GetAllPaymentTransactionByFilter/GetAllPaymentTransactionByFilterQuery.cs index e9205d9..b85d1c0 100644 --- a/src/CMSMicroservice.Application/PaymentTransactionCQ/Queries/GetAllPaymentTransactionByFilter/GetAllPaymentTransactionByFilterQuery.cs +++ b/src/CMSMicroservice.Application/PaymentTransactionCQ/Queries/GetAllPaymentTransactionByFilter/GetAllPaymentTransactionByFilterQuery.cs @@ -43,4 +43,5 @@ public class GetAllPaymentTransactionByFilterResponseModel public long? TransactionId { get; set; } public string? Description { get; set; } public DateTimeOffset Created { get; set; } + public string? UserFullName { get; set; } } diff --git a/src/CMSMicroservice.Application/PaymentTransactionCQ/Queries/GetAllPaymentTransactionByFilter/GetAllPaymentTransactionByFilterQueryHandler.cs b/src/CMSMicroservice.Application/PaymentTransactionCQ/Queries/GetAllPaymentTransactionByFilter/GetAllPaymentTransactionByFilterQueryHandler.cs index 57e3e0f..dfc00b3 100644 --- a/src/CMSMicroservice.Application/PaymentTransactionCQ/Queries/GetAllPaymentTransactionByFilter/GetAllPaymentTransactionByFilterQueryHandler.cs +++ b/src/CMSMicroservice.Application/PaymentTransactionCQ/Queries/GetAllPaymentTransactionByFilter/GetAllPaymentTransactionByFilterQueryHandler.cs @@ -19,30 +19,43 @@ public class GetAllPaymentTransactionByFilterQueryHandler .ApplyPaymentTransactionFilter(request.Filter) .ApplyOrder(sortBy: request.SortBy ?? "-Created"); + var models = await ( + from x in query.PaginatedListAsync(paginationState: request.PaginationState) + join u in _context.Users.AsNoTracking() on x.UserId equals u.Id into ug + from u in ug.DefaultIfEmpty() + select new GetAllPaymentTransactionByFilterResponseModel + { + Id = x.Id, + UserId = x.UserId, + Mobile = x.Mobile, + GatewayProvider = x.GatewayProvider, + Amount = x.Amount, + Authority = x.Authority, + PaymentStatus = x.PaymentStatus, + VerificationStatusCode = x.VerificationStatusCode, + VerificationStatusMessage = x.VerificationStatusMessage, + RefId = x.RefId, + CardPan = x.CardPan, + OrderId = x.OrderId, + TransactionId = x.TransactionId, + Description = x.Description, + Created = x.Created, + UserFullName = u != null + ? ((u.FirstName ?? "") + " " + (u.LastName ?? "")).Trim() + : null + }) + .ToListAsync(cancellationToken); + + foreach (var model in models) + { + if (string.IsNullOrWhiteSpace(model.UserFullName)) + model.UserFullName = "—"; + } + return new GetAllPaymentTransactionByFilterResponseDto { MetaData = await query.GetMetaData(request.PaginationState, cancellationToken), - Models = await query - .PaginatedListAsync(paginationState: request.PaginationState) - .Select(x => new GetAllPaymentTransactionByFilterResponseModel - { - Id = x.Id, - UserId = x.UserId, - Mobile = x.Mobile, - GatewayProvider = x.GatewayProvider, - Amount = x.Amount, - Authority = x.Authority, - PaymentStatus = x.PaymentStatus, - VerificationStatusCode = x.VerificationStatusCode, - VerificationStatusMessage = x.VerificationStatusMessage, - RefId = x.RefId, - CardPan = x.CardPan, - OrderId = x.OrderId, - TransactionId = x.TransactionId, - Description = x.Description, - Created = x.Created - }) - .ToListAsync(cancellationToken) + Models = models }; } } diff --git a/src/CMSMicroservice.Application/TransactionsCQ/Queries/GetAllTransactionsByFilter/GetAllTransactionsByFilterQueryHandler.cs b/src/CMSMicroservice.Application/TransactionsCQ/Queries/GetAllTransactionsByFilter/GetAllTransactionsByFilterQueryHandler.cs index a2272ab..7fa2b1a 100644 --- a/src/CMSMicroservice.Application/TransactionsCQ/Queries/GetAllTransactionsByFilter/GetAllTransactionsByFilterQueryHandler.cs +++ b/src/CMSMicroservice.Application/TransactionsCQ/Queries/GetAllTransactionsByFilter/GetAllTransactionsByFilterQueryHandler.cs @@ -1,3 +1,5 @@ +using CMSMicroservice.Application.Common; + namespace CMSMicroservice.Application.TransactionsCQ.Queries.GetAllTransactionsByFilter; public class GetAllTransactionsByFilterQueryHandler : IRequestHandler @@ -16,23 +18,106 @@ public class GetAllTransactionsByFilterQueryHandler : IRequestHandler new GetAllTransactionsByFilterResponseModel + { + Id = x.Id, + Amount = x.Amount, + Description = x.Description, + PaymentStatus = x.PaymentStatus, + PaymentDate = x.PaymentDate, + RefId = x.RefId, + Type = x.Type, + Created = x.Created + }) + .ToListAsync(cancellationToken); + + await PopulateUserInfoAsync(models, cancellationToken); + return new GetAllTransactionsByFilterResponseDto { MetaData = await query.GetMetaData(request.PaginationState, cancellationToken), - Models = await query - .PaginatedListAsync(paginationState: request.PaginationState) - .Select(x => new GetAllTransactionsByFilterResponseModel - { - Id = x.Id, - Amount = x.Amount, - Description = x.Description, - PaymentStatus = x.PaymentStatus, - PaymentDate = x.PaymentDate, - RefId = x.RefId, - Type = x.Type, - Created = x.Created - }) - .ToListAsync(cancellationToken) + Models = models }; } + + private async Task PopulateUserInfoAsync( + List models, + CancellationToken cancellationToken) + { + if (models.Count == 0) + return; + + var txIds = models.Select(m => m.Id).ToList(); + var userByTx = new Dictionary(); + + void Merge(IEnumerable<(long TxId, long UserId)> rows) + { + foreach (var (txId, userId) in rows) + { + if (!userByTx.ContainsKey(txId)) + userByTx[txId] = userId; + } + } + + // Priority: PaymentTransactions → UserPackagePurchases → UserOrders → ManualPayments + var fromPt = await _context.PaymentTransactions.AsNoTracking() + .Where(x => x.TransactionId != null && txIds.Contains(x.TransactionId.Value) && x.UserId != null) + .Select(x => new { TxId = x.TransactionId!.Value, UserId = x.UserId!.Value }) + .ToListAsync(cancellationToken); + Merge(fromPt.Select(x => (x.TxId, x.UserId))); + + var fromUpp = await _context.UserPackagePurchases.AsNoTracking() + .Where(x => x.TransactionId != null && txIds.Contains(x.TransactionId.Value)) + .Select(x => new { TxId = x.TransactionId!.Value, UserId = x.UserId }) + .ToListAsync(cancellationToken); + Merge(fromUpp.Select(x => (x.TxId, x.UserId))); + + var fromOrders = await _context.UserOrders.AsNoTracking() + .Where(x => x.TransactionId != null && txIds.Contains(x.TransactionId.Value)) + .Select(x => new { TxId = x.TransactionId!.Value, UserId = x.UserId }) + .ToListAsync(cancellationToken); + Merge(fromOrders.Select(x => (x.TxId, x.UserId))); + + var fromManual = await _context.ManualPayments.AsNoTracking() + .Where(x => x.TransactionId != null && txIds.Contains(x.TransactionId.Value)) + .Select(x => new { TxId = x.TransactionId!.Value, UserId = x.UserId }) + .ToListAsync(cancellationToken); + Merge(fromManual.Select(x => (x.TxId, x.UserId))); + + if (userByTx.Count == 0) + { + foreach (var model in models) + model.UserFullName = "—"; + return; + } + + var userIds = userByTx.Values.Distinct().ToList(); + var users = await _context.Users.AsNoTracking() + .Where(u => userIds.Contains(u.Id)) + .Select(u => new { u.Id, u.FirstName, u.LastName, u.Mobile }) + .ToDictionaryAsync(u => u.Id, cancellationToken); + + foreach (var model in models) + { + if (!userByTx.TryGetValue(model.Id, out var userId)) + { + model.UserFullName = "—"; + continue; + } + + model.UserId = userId; + if (users.TryGetValue(userId, out var user)) + { + model.Mobile = user.Mobile; + var name = UserDisplayName.From(user.FirstName, user.LastName, user.Mobile, user.Id); + model.UserFullName = string.IsNullOrWhiteSpace(name) ? "—" : name; + } + else + { + model.UserFullName = "—"; + } + } + } } diff --git a/src/CMSMicroservice.Application/TransactionsCQ/Queries/GetAllTransactionsByFilter/GetAllTransactionsByFilterResponseDto.cs b/src/CMSMicroservice.Application/TransactionsCQ/Queries/GetAllTransactionsByFilter/GetAllTransactionsByFilterResponseDto.cs index ed4e290..a6ca55a 100644 --- a/src/CMSMicroservice.Application/TransactionsCQ/Queries/GetAllTransactionsByFilter/GetAllTransactionsByFilterResponseDto.cs +++ b/src/CMSMicroservice.Application/TransactionsCQ/Queries/GetAllTransactionsByFilter/GetAllTransactionsByFilterResponseDto.cs @@ -25,4 +25,7 @@ public class GetAllTransactionsByFilterResponseDto // public TransactionType Type { get; set; } public DateTime Created { get; set; } + public long? UserId { get; set; } + public string? UserFullName { get; set; } + public string? Mobile { get; set; } } diff --git a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj index ca8475e..d7341af 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.213 + 0.0.214 None False False diff --git a/src/CMSMicroservice.Protobuf/Protos/paymenttransaction.proto b/src/CMSMicroservice.Protobuf/Protos/paymenttransaction.proto index 488c6b6..e7865ec 100644 --- a/src/CMSMicroservice.Protobuf/Protos/paymenttransaction.proto +++ b/src/CMSMicroservice.Protobuf/Protos/paymenttransaction.proto @@ -66,6 +66,7 @@ message PaymentTransactionModel google.protobuf.Int64Value transaction_id = 13; string description = 14; google.protobuf.Timestamp created_at = 15; + string user_full_name = 16; } message GetPaymentTransactionSummaryRequest diff --git a/src/CMSMicroservice.Protobuf/Protos/transactions.proto b/src/CMSMicroservice.Protobuf/Protos/transactions.proto index 07a78ff..801f473 100644 --- a/src/CMSMicroservice.Protobuf/Protos/transactions.proto +++ b/src/CMSMicroservice.Protobuf/Protos/transactions.proto @@ -192,6 +192,9 @@ message GetAllTransactionsByFilterResponseModel messages.TransactionType type = 7; } google.protobuf.Timestamp created = 8; + google.protobuf.Int64Value user_id = 9; + string user_full_name = 10; + google.protobuf.StringValue mobile = 11; } message GetTransactionSummaryRequest diff --git a/src/CMSMicroservice.WebApi/Common/Mappings/PaymentTransactionProfile.cs b/src/CMSMicroservice.WebApi/Common/Mappings/PaymentTransactionProfile.cs index 00eba3c..3d41667 100644 --- a/src/CMSMicroservice.WebApi/Common/Mappings/PaymentTransactionProfile.cs +++ b/src/CMSMicroservice.WebApi/Common/Mappings/PaymentTransactionProfile.cs @@ -46,7 +46,8 @@ public class PaymentTransactionProfile : IRegister .Map(dest => dest.OrderId, src => src.OrderId ?? string.Empty) .Map(dest => dest.TransactionId, src => src.TransactionId) .Map(dest => dest.Description, src => src.Description ?? string.Empty) - .Map(dest => dest.CreatedAt, src => Timestamp.FromDateTimeOffset(src.Created)); + .Map(dest => dest.CreatedAt, src => Timestamp.FromDateTimeOffset(src.Created)) + .Map(dest => dest.UserFullName, src => src.UserFullName ?? string.Empty); config.NewConfig() .Map(dest => dest.Filter, src => src.Filter); diff --git a/src/CMSMicroservice.WebApi/Common/Mappings/TransactionsProfile.cs b/src/CMSMicroservice.WebApi/Common/Mappings/TransactionsProfile.cs index f9228ad..defecb3 100644 --- a/src/CMSMicroservice.WebApi/Common/Mappings/TransactionsProfile.cs +++ b/src/CMSMicroservice.WebApi/Common/Mappings/TransactionsProfile.cs @@ -40,7 +40,10 @@ public class TransactionsProfile : IRegister .Map(dest => dest.PaymentDate, src => src.PaymentDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(src.PaymentDate.Value, DateTimeKind.Utc)) : null) .Map(dest => dest.RefId, src => src.RefId) .Map(dest => dest.Type, src => src.Type) - .Map(dest => dest.Created, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.Created, DateTimeKind.Utc))); + .Map(dest => dest.Created, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.Created, DateTimeKind.Utc))) + .Map(dest => dest.UserId, src => src.UserId) + .Map(dest => dest.UserFullName, src => src.UserFullName ?? string.Empty) + .Map(dest => dest.Mobile, src => src.Mobile); config.NewConfig() .Map(dest => dest.Filter, src => src.Filter);