From e882f09955a7bb85113caa0fd4ea46c348b6f79d Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 14 Aug 2026 03:18:58 +0330 Subject: [PATCH 1/3] feat(user): enhance GetAllUserByFilter query response with additional fields and update Protobuf definitions - Added DefaultAddress and DefaultPostalCode properties to GetAllUserByFilterResponseDto for improved user data representation. - Updated GetAllUserByFilterQueryHandler to populate new response fields with relevant data. - Modified Protobuf definitions to include new fields in GetAllUserByFilterResponseModel. - Bumped Protobuf project version to reflect the addition of new features. --- README.md | 2 +- .../GetAllUserByFilterQueryHandler.cs | 35 ++++++++++++++++--- .../GetAllUserByFilterResponseDto.cs | 4 +++ .../CMSMicroservice.Protobuf.csproj | 2 +- .../Protos/user.proto | 2 ++ .../Common/Mappings/UserProfile.cs | 7 ++-- 6 files changed, 43 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9fd7663..06b3f36 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ # CMS Microservice Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md). - \ No newline at end of file + \ No newline at end of file diff --git a/src/CMSMicroservice.Application/UserCQ/Queries/GetAllUserByFilter/GetAllUserByFilterQueryHandler.cs b/src/CMSMicroservice.Application/UserCQ/Queries/GetAllUserByFilter/GetAllUserByFilterQueryHandler.cs index d2cc23e..1116df0 100644 --- a/src/CMSMicroservice.Application/UserCQ/Queries/GetAllUserByFilter/GetAllUserByFilterQueryHandler.cs +++ b/src/CMSMicroservice.Application/UserCQ/Queries/GetAllUserByFilter/GetAllUserByFilterQueryHandler.cs @@ -17,7 +17,7 @@ public class GetAllUserByFilterQueryHandler : IRequestHandler request.Filter.SearchText == null || + .Where(x => request.Filter.SearchText == null || x.FirstName.Contains(request.Filter.SearchText) || x.LastName.Contains(request.Filter.SearchText) || x.Mobile.Contains(request.Filter.SearchText) || @@ -34,14 +34,39 @@ public class GetAllUserByFilterQueryHandler : IRequestHandler request.Filter.SmsNotifications == null || x.SmsNotifications == request.Filter.SmsNotifications) .Where(x => request.Filter.EmailNotifications == null || x.EmailNotifications == request.Filter.EmailNotifications) .Where(x => request.Filter.PushNotifications == null || x.PushNotifications == request.Filter.PushNotifications) - .Where(x => request.Filter.BirthDate == null || x.BirthDate == request.Filter.BirthDate) -; + .Where(x => request.Filter.BirthDate == null || x.BirthDate == request.Filter.BirthDate); } + return new GetAllUserByFilterResponseDto { MetaData = await query.GetMetaData(request.PaginationState, cancellationToken), Models = await query.PaginatedListAsync(paginationState: request.PaginationState) - .ProjectToType().ToListAsync(cancellationToken) - }; + .Select(x => new GetAllUserByFilterResponseModel + { + Id = x.Id, + FirstName = x.FirstName, + LastName = x.LastName, + Mobile = x.Mobile, + NationalCode = x.NationalCode, + AvatarPath = x.AvatarPath, + NetworkParentId = x.NetworkParentId, + ReferralCode = x.ReferralCode, + IsMobileVerified = x.IsMobileVerified, + MobileVerifiedAt = x.MobileVerifiedAt, + EmailNotifications = x.EmailNotifications, + SmsNotifications = x.SmsNotifications, + PushNotifications = x.PushNotifications, + BirthDate = x.BirthDate, + DefaultAddress = x.UserAddresses + .Where(a => a.IsDefault && !a.IsDeleted) + .Select(a => a.Address) + .FirstOrDefault() ?? string.Empty, + DefaultPostalCode = x.UserAddresses + .Where(a => a.IsDefault && !a.IsDeleted) + .Select(a => a.PostalCode) + .FirstOrDefault() ?? string.Empty + }) + .ToListAsync(cancellationToken) + }; } } diff --git a/src/CMSMicroservice.Application/UserCQ/Queries/GetAllUserByFilter/GetAllUserByFilterResponseDto.cs b/src/CMSMicroservice.Application/UserCQ/Queries/GetAllUserByFilter/GetAllUserByFilterResponseDto.cs index 617fcba..399b13c 100644 --- a/src/CMSMicroservice.Application/UserCQ/Queries/GetAllUserByFilter/GetAllUserByFilterResponseDto.cs +++ b/src/CMSMicroservice.Application/UserCQ/Queries/GetAllUserByFilter/GetAllUserByFilterResponseDto.cs @@ -36,4 +36,8 @@ public class GetAllUserByFilterResponseDto public bool PushNotifications { get; set; } //تاریخ تولد public DateTime? BirthDate { get; set; } + //آدرس پیش‌فرض + public string DefaultAddress { get; set; } = string.Empty; + //کدپستی پیش‌فرض + public string DefaultPostalCode { get; set; } = string.Empty; } diff --git a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj index 6d7c475..685ed49 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.208 + 0.0.209 None False False diff --git a/src/CMSMicroservice.Protobuf/Protos/user.proto b/src/CMSMicroservice.Protobuf/Protos/user.proto index 367470c..8465bd4 100644 --- a/src/CMSMicroservice.Protobuf/Protos/user.proto +++ b/src/CMSMicroservice.Protobuf/Protos/user.proto @@ -238,6 +238,8 @@ message GetAllUserByFilterResponseModel bool sms_notifications = 12; bool push_notifications = 13; google.protobuf.Timestamp birth_date = 14; + string default_address = 15; + string default_postal_code = 16; } message GetJwtTokenRequest { diff --git a/src/CMSMicroservice.WebApi/Common/Mappings/UserProfile.cs b/src/CMSMicroservice.WebApi/Common/Mappings/UserProfile.cs index 02c294f..9a7553e 100644 --- a/src/CMSMicroservice.WebApi/Common/Mappings/UserProfile.cs +++ b/src/CMSMicroservice.WebApi/Common/Mappings/UserProfile.cs @@ -4,7 +4,10 @@ public class UserProfile : IRegister { void IRegister.Register(TypeAdapterConfig config) { - //config.NewConfig() - // .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}"); + config.NewConfig() + .Map(dest => dest.ParentId, src => src.NetworkParentId) + .Map(dest => dest.DefaultAddress, src => src.DefaultAddress ?? string.Empty) + .Map(dest => dest.DefaultPostalCode, src => src.DefaultPostalCode ?? string.Empty); } } From 8ec0910cdffdd66328df28a61e49eddbab0f465b Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sun, 23 Aug 2026 22:32:36 +0330 Subject: [PATCH 2/3] feat(jwt): enhance JWT token generation with user address check - Updated GenerateJwtTokenService to include IApplicationDbContext for accessing user addresses. - Added logic to check if a user has an address and included this information as a claim in the generated JWT token. - Improved overall token generation process by incorporating additional user data for better context. --- .../Services/GenerateJwtTokenService.cs | 11 +- ...erPackagePurchases_FromClubMemberships.sql | 257 ++++++++++ .../Fix_Daya_ActiveExcel_ProcessMissing.sql | 465 ++++++++++++++++++ .../Fix_WrongDaya_ToManual_PurchaseMethod.sql | 157 ++++++ ...Rollback_Backfill_UserPackagePurchases.sql | 116 +++++ ...date_DayaLoanContracts_ContractNumbers.sql | 145 ++++++ 6 files changed, 1149 insertions(+), 2 deletions(-) create mode 100644 src/dbbkup/Backfill_UserPackagePurchases_FromClubMemberships.sql create mode 100644 src/dbbkup/Fix_Daya_ActiveExcel_ProcessMissing.sql create mode 100644 src/dbbkup/Fix_WrongDaya_ToManual_PurchaseMethod.sql create mode 100644 src/dbbkup/Rollback_Backfill_UserPackagePurchases.sql create mode 100644 src/dbbkup/Update_DayaLoanContracts_ContractNumbers.sql diff --git a/src/CMSMicroservice.Infrastructure/Services/GenerateJwtTokenService.cs b/src/CMSMicroservice.Infrastructure/Services/GenerateJwtTokenService.cs index 5a6533b..922a6d3 100644 --- a/src/CMSMicroservice.Infrastructure/Services/GenerateJwtTokenService.cs +++ b/src/CMSMicroservice.Infrastructure/Services/GenerateJwtTokenService.cs @@ -1,5 +1,6 @@ -using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Domain.Entities; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using System.Collections.Generic; @@ -13,10 +14,12 @@ namespace CMSMicroservice.Infrastructure.Services; public class GenerateJwtTokenService : IGenerateJwtToken { private readonly IConfiguration _configuration; + private readonly IApplicationDbContext _context; - public GenerateJwtTokenService(IConfiguration configuration) + public GenerateJwtTokenService(IConfiguration configuration, IApplicationDbContext context) { _configuration = configuration; + _context = context; } public async Task GenerateJwtToken(User user, int? expiryDays = null) @@ -58,6 +61,10 @@ public class GenerateJwtTokenService : IGenerateJwtToken claims.Add(new Claim("HasPurchasedPackage", (user.PackagePurchaseMethod != PackagePurchaseMethod.None).ToString())); + var hasAddress = await _context.UserAddresses + .AnyAsync(a => a.UserId == user.Id && !a.IsDeleted); + claims.Add(new Claim("HasAddress", hasAddress.ToString())); + if (user.UserRoles != null && user.UserRoles.Any()) { foreach (var userRole in user.UserRoles) diff --git a/src/dbbkup/Backfill_UserPackagePurchases_FromClubMemberships.sql b/src/dbbkup/Backfill_UserPackagePurchases_FromClubMemberships.sql new file mode 100644 index 0000000..b074cda --- /dev/null +++ b/src/dbbkup/Backfill_UserPackagePurchases_FromClubMemberships.sql @@ -0,0 +1,257 @@ +/* + One-time cleanup: ClubMemberships without a successful UserPackagePurchases row. + + Goal after this script: + every non-deleted ClubMembership user has >= 1 successful UPP + (TransactionId IS NULL OR Transaction.PaymentStatus = 0 / Success) + + Priority for missing rows: + 1) ManualPayments Approved + successful Transaction -> reuse same TransactionId + PurchaseMethod = Manual (3) + 2) else ClubMembershipCycles / membership dates -> create synthetic Success Transaction + UPP + PurchaseMethod priority: + Cycle.PurchaseMethod (if not None) + -> ClubMembership.PurchaseMethod (if not None) + -> Users.PackagePurchaseMethod (if not None) + -> Manual (3) as last resort + + Safe to re-run (skips users who already have a successful UPP). + + Run against CMS database once, then verify Orphans left = 0. +*/ + +SET NOCOUNT ON; +SET XACT_ABORT ON; + +BEGIN TRAN; + +------------------------------------------------------------ +-- 1) From ManualPayments (reuse existing Success transaction) +------------------------------------------------------------ +;WITH OrphanClubUsers AS ( + SELECT m.UserId, + COALESCE(NULLIF(m.LastPackageId, 0), NULLIF(m.FirstPackageId, 0)) AS PackageIdHint + FROM CMS.ClubMemberships m + WHERE m.IsDeleted = 0 + AND NOT EXISTS ( + SELECT 1 + FROM CMS.UserPackagePurchases upp + LEFT JOIN CMS.Transactions t ON t.Id = upp.TransactionId AND t.IsDeleted = 0 + WHERE upp.UserId = m.UserId + AND upp.IsDeleted = 0 + AND (upp.TransactionId IS NULL OR t.PaymentStatus = 0) + ) +), +BestManual AS ( + SELECT mp.UserId, + mp.Id AS ManualPaymentId, + mp.Amount, + mp.TransactionId, + COALESCE(mp.ApprovedAt, mp.Created) AS PurchasedAt, + ROW_NUMBER() OVER ( + PARTITION BY mp.UserId + ORDER BY COALESCE(mp.ApprovedAt, mp.Created) DESC, mp.Id DESC + ) AS rn + FROM CMS.ManualPayments mp + INNER JOIN CMS.Transactions t ON t.Id = mp.TransactionId AND t.IsDeleted = 0 AND t.PaymentStatus = 0 + WHERE mp.IsDeleted = 0 + AND mp.Status = 1 -- Approved + AND mp.TransactionId IS NOT NULL +), +BasePkg AS ( + SELECT TOP (1) Id AS PackageId, Price + FROM CMS.Packages + WHERE IsDeleted = 0 AND IsBasePackage = 1 + ORDER BY Id +) +INSERT INTO CMS.UserPackagePurchases +( + UserId, PackageId, PurchaseMethod, PurchasedAt, Amount, + OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted +) +SELECT + o.UserId, + COALESCE(o.PackageIdHint, b.PackageId), + 3, -- Manual + bm.PurchasedAt, + bm.Amount, + NULL, + bm.TransactionId, + SYSUTCDATETIME(), + NULL, + SYSUTCDATETIME(), + NULL, + 0 +FROM OrphanClubUsers o +INNER JOIN BestManual bm ON bm.UserId = o.UserId AND bm.rn = 1 +CROSS JOIN BasePkg b +WHERE COALESCE(o.PackageIdHint, b.PackageId) IS NOT NULL; + +DECLARE @FromManual INT = @@ROWCOUNT; +PRINT CONCAT('Inserted from ManualPayments: ', @FromManual); + +------------------------------------------------------------ +-- 2) Remaining orphans from Cycle / membership dates +------------------------------------------------------------ +;WITH OrphanClubUsers AS ( + SELECT + m.UserId, + m.PurchaseMethod, + u.PackagePurchaseMethod AS UserPurchaseMethod, + COALESCE(NULLIF(m.LastPackageId, 0), NULLIF(m.FirstPackageId, 0)) AS MembPackageId, + COALESCE(m.FirstActivationDate, m.LastActivationDate, m.ActivatedAt, m.Created) AS MembPurchasedAt + FROM CMS.ClubMemberships m + INNER JOIN CMS.Users u ON u.Id = m.UserId AND u.IsDeleted = 0 + WHERE m.IsDeleted = 0 + AND NOT EXISTS ( + SELECT 1 + FROM CMS.UserPackagePurchases upp + LEFT JOIN CMS.Transactions t ON t.Id = upp.TransactionId AND t.IsDeleted = 0 + WHERE upp.UserId = m.UserId + AND upp.IsDeleted = 0 + AND (upp.TransactionId IS NULL OR t.PaymentStatus = 0) + ) +), +BestCycle AS ( + SELECT + c.UserId, + c.PackageId, + c.PackageAmount, + c.PurchaseMethod, + c.PackagePurchasedAt, + ROW_NUMBER() OVER (PARTITION BY c.UserId ORDER BY c.CycleNumber ASC, c.Id ASC) AS rn + FROM CMS.ClubMembershipCycles c + WHERE c.IsDeleted = 0 +), +BasePkg AS ( + SELECT TOP (1) Id AS PackageId, Price + FROM CMS.Packages + WHERE IsDeleted = 0 AND IsBasePackage = 1 + ORDER BY Id +), +ToInsert AS ( + SELECT + o.UserId, + COALESCE(NULLIF(bc.PackageId, 0), o.MembPackageId, b.PackageId) AS PackageId, + CASE + WHEN bc.PurchaseMethod IS NOT NULL AND bc.PurchaseMethod <> 0 THEN bc.PurchaseMethod + WHEN o.PurchaseMethod IS NOT NULL AND o.PurchaseMethod <> 0 THEN o.PurchaseMethod + WHEN o.UserPurchaseMethod IS NOT NULL AND o.UserPurchaseMethod <> 0 THEN o.UserPurchaseMethod + ELSE 3 -- Manual (last resort) + END AS PurchaseMethod, + COALESCE(bc.PackagePurchasedAt, o.MembPurchasedAt, SYSUTCDATETIME()) AS PurchasedAt, + CASE + WHEN bc.PackageAmount IS NOT NULL AND bc.PackageAmount > 0 THEN bc.PackageAmount + ELSE COALESCE(p.Price, b.Price, 0) + END AS Amount + FROM OrphanClubUsers o + LEFT JOIN BestCycle bc ON bc.UserId = o.UserId AND bc.rn = 1 + CROSS JOIN BasePkg b + LEFT JOIN CMS.Packages p ON p.Id = COALESCE(NULLIF(bc.PackageId, 0), o.MembPackageId, b.PackageId) AND p.IsDeleted = 0 +) +SELECT * +INTO #BackfillCandidates +FROM ToInsert +WHERE PackageId IS NOT NULL AND Amount > 0; + +DECLARE @Remaining INT = (SELECT COUNT(*) FROM #BackfillCandidates); +PRINT CONCAT('Remaining orphans to synthetic backfill: ', @Remaining); + +DECLARE @UserId BIGINT, @PackageId BIGINT, @PurchaseMethod INT, @PurchasedAt DATETIME2, @Amount BIGINT; +DECLARE @TxId BIGINT; + +DECLARE cur CURSOR LOCAL FAST_FORWARD FOR + SELECT UserId, PackageId, PurchaseMethod, PurchasedAt, Amount + FROM #BackfillCandidates; + +OPEN cur; +FETCH NEXT FROM cur INTO @UserId, @PackageId, @PurchaseMethod, @PurchasedAt, @Amount; + +DECLARE @Synthetic INT = 0; + +WHILE @@FETCH_STATUS = 0 +BEGIN + INSERT INTO CMS.Transactions + ( + Amount, Description, PaymentStatus, PaymentDate, RefId, Type, + Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted + ) + VALUES + ( + @Amount, + CONCAT(N'بک‌فیل عضویت باشگاه — UserId ', @UserId), + 0, -- Success + @PurchasedAt, + CONCAT('UPP-BACKFILL-', @UserId, '-', @PackageId), + 2, -- DepositExternal1 + SYSUTCDATETIME(), NULL, SYSUTCDATETIME(), NULL, 0 + ); + + SET @TxId = SCOPE_IDENTITY(); + + INSERT INTO CMS.UserPackagePurchases + ( + UserId, PackageId, PurchaseMethod, PurchasedAt, Amount, + OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted + ) + VALUES + ( + @UserId, @PackageId, @PurchaseMethod, @PurchasedAt, @Amount, + NULL, @TxId, SYSUTCDATETIME(), NULL, SYSUTCDATETIME(), NULL, 0 + ); + + SET @Synthetic += 1; + FETCH NEXT FROM cur INTO @UserId, @PackageId, @PurchaseMethod, @PurchasedAt, @Amount; +END + +CLOSE cur; +DEALLOCATE cur; + +PRINT CONCAT('Inserted synthetic UPP rows: ', @Synthetic); + +DROP TABLE #BackfillCandidates; + +------------------------------------------------------------ +-- Verify invariant +------------------------------------------------------------ +DECLARE @ClubCount INT = +( + SELECT COUNT(*) FROM CMS.ClubMemberships WHERE IsDeleted = 0 +); + +DECLARE @OrphansLeft INT = +( + SELECT COUNT(*) + FROM CMS.ClubMemberships m + WHERE m.IsDeleted = 0 + AND NOT EXISTS ( + SELECT 1 + FROM CMS.UserPackagePurchases upp + LEFT JOIN CMS.Transactions t ON t.Id = upp.TransactionId AND t.IsDeleted = 0 + WHERE upp.UserId = m.UserId + AND upp.IsDeleted = 0 + AND (upp.TransactionId IS NULL OR t.PaymentStatus = 0) + ) +); + +DECLARE @SuccessfulBuyers INT = +( + SELECT COUNT(DISTINCT upp.UserId) + FROM CMS.UserPackagePurchases upp + LEFT JOIN CMS.Transactions t ON t.Id = upp.TransactionId AND t.IsDeleted = 0 + WHERE upp.IsDeleted = 0 + AND (upp.TransactionId IS NULL OR t.PaymentStatus = 0) +); + +PRINT CONCAT('ClubMemberships: ', @ClubCount); +PRINT CONCAT('Distinct successful UPP users: ', @SuccessfulBuyers); +PRINT CONCAT('Orphans left (must be 0): ', @OrphansLeft); + +IF @OrphansLeft > 0 +BEGIN + ROLLBACK; + THROW 50001, 'Backfill incomplete — orphans remain; transaction rolled back.', 1; +END + +COMMIT; +PRINT 'Backfill committed successfully.'; diff --git a/src/dbbkup/Fix_Daya_ActiveExcel_ProcessMissing.sql b/src/dbbkup/Fix_Daya_ActiveExcel_ProcessMissing.sql new file mode 100644 index 0000000..2cdd54b --- /dev/null +++ b/src/dbbkup/Fix_Daya_ActiveExcel_ProcessMissing.sql @@ -0,0 +1,465 @@ +/* + Staging fix: complete post-Daya processing for Excel rows with status «فعال شده» + that are NOT_PROCESSED or PARTIAL. + + Classification: + DONE — credit + processed + Daya UPP + PARTIAL — HasReceivedDayaCredit OR IsProcessed OR existing «اعتبار دایا» Tx in wallet + NOT_PROCESSED — none of the above (will create Tx + charge) + + If ExistingDayaTxId found (wallet RefrenceId → Tx Description LIKE '%اعتبار دایا%' + OR Tx.RefId = ContractNumber): REUSE that Tx — do NOT create Transaction / do NOT charge wallet. + + Special UserId mapping (shared NationalCode): + C4_D8815963 / 2490371436 → UserId 50 + C4_G8945289 / 2490371436 → UserId 51 + + @Commit = 0 → dry-run (ROLLBACK) + @Commit = 1 → apply +*/ + +SET NOCOUNT ON; +SET XACT_ABORT ON; + +DECLARE @Commit BIT = 0; -- << set 1 to apply + +DECLARE @Now DATETIME2 = SYSUTCDATETIME(); +DECLARE @PackageId BIGINT; +DECLARE @PackagePrice BIGINT; +DECLARE @DiscountMultiplier DECIMAL(18,4); +DECLARE @DiscountAmount BIGINT; + +SELECT TOP (1) + @PackageId = Id, + @PackagePrice = Price, + @DiscountMultiplier = DiscountMultiplier +FROM CMS.Packages +WHERE IsDeleted = 0 AND IsBasePackage = 1 +ORDER BY Id; + +IF @PackageId IS NULL + THROW 50001, 'Base package not found.', 1; + +SET @DiscountAmount = CAST(@PackagePrice * @DiscountMultiplier AS BIGINT); + +PRINT CONCAT('PackageId=', @PackageId, ' Price=', @PackagePrice, ' Discount=', @DiscountAmount); + +------------------------------------------------------------ +-- Targets: Excel «فعال شده» +------------------------------------------------------------ +DECLARE @Targets TABLE ( + ContractNumber NVARCHAR(50) NOT NULL, + NationalCode NVARCHAR(20) NOT NULL, + UserIdHint BIGINT NULL -- explicit override when NC is shared +); + +INSERT INTO @Targets (ContractNumber, NationalCode, UserIdHint) VALUES +(N'C4_H10706299', N'4060667961', NULL), +(N'C4_P9320383', N'2572729636', NULL), +(N'C4_Q9465813', N'2480017796', NULL), +(N'C4_B9348378', N'0452314488', NULL), +(N'C4_X9630520', N'0680164286', NULL), +(N'C4_B9813553', N'0055680364', NULL), -- may match DayaLoanContracts.NationalCode if Users.NC differs +(N'C4_M9994735', N'0793851572', NULL), +(N'C4_S9301452', N'0082242925', NULL), +(N'C4_T9961063', N'0312342993', NULL), +(N'C4_L9524515', N'0024757489', NULL), +(N'C4_Q9947535', N'2480259668', NULL), +(N'C4_T9892089', N'2451477016', NULL), +(N'C4_W8785358', N'0322922720', NULL), +(N'C4_D8815963', N'2490371436', 50), -- earlier activation → user 50 +(N'C4_G8945289', N'2490371436', 51), -- later activation → user 51 +(N'C4_J8388494', N'2002637903', NULL); + +DECLARE @Work TABLE ( + ContractNumber NVARCHAR(50), + NationalCode NVARCHAR(20), + UserId BIGINT, + DayaContractId BIGINT NULL, + HasCredit BIT, + IsProcessed BIT, + HasDayaUpp BIT, + ExistingDayaTxId BIGINT NULL, -- Tx از قبل با شرح «اعتبار دایا» / RefId قرارداد (شارژ شده) + Verdict NVARCHAR(20) +); + +INSERT INTO @Work (ContractNumber, NationalCode, UserId, DayaContractId, HasCredit, IsProcessed, HasDayaUpp, ExistingDayaTxId, Verdict) +SELECT + t.ContractNumber, + t.NationalCode, + u.Id, + d.Id, + CAST(ISNULL(u.HasReceivedDayaCredit, 0) AS BIT), + CAST(ISNULL(d.IsProcessed, 0) AS BIT), + CAST(CASE WHEN EXISTS ( + SELECT 1 + FROM CMS.UserPackagePurchases upp + LEFT JOIN CMS.Transactions tx ON tx.Id = upp.TransactionId AND tx.IsDeleted = 0 + WHERE upp.UserId = u.Id + AND upp.IsDeleted = 0 + AND upp.PurchaseMethod = 1 + AND (upp.TransactionId IS NULL OR tx.PaymentStatus = 0) + ) THEN 1 ELSE 0 END AS BIT), + etx.TxId, + CASE + WHEN u.Id IS NULL THEN N'NO_USER' + WHEN ISNULL(u.HasReceivedDayaCredit, 0) = 1 + AND ISNULL(d.IsProcessed, 0) = 1 + AND EXISTS ( + SELECT 1 + FROM CMS.UserPackagePurchases upp + LEFT JOIN CMS.Transactions tx ON tx.Id = upp.TransactionId AND tx.IsDeleted = 0 + WHERE upp.UserId = u.Id + AND upp.IsDeleted = 0 + AND upp.PurchaseMethod = 1 + AND (upp.TransactionId IS NULL OR tx.PaymentStatus = 0) + ) THEN N'DONE' + -- قبلاً شارژ دایا در کیف/تراکنش هست ولی فلگ/UPP/قرارداد کامل نیست + WHEN etx.TxId IS NOT NULL THEN N'PARTIAL' + WHEN ISNULL(u.HasReceivedDayaCredit, 0) = 1 + OR ISNULL(d.IsProcessed, 0) = 1 THEN N'PARTIAL' + ELSE N'NOT_PROCESSED' + END +FROM @Targets t +OUTER APPLY ( + SELECT TOP (1) u0.* + FROM CMS.Users u0 + WHERE u0.IsDeleted = 0 + AND ( + (t.UserIdHint IS NOT NULL AND u0.Id = t.UserIdHint) + OR (t.UserIdHint IS NULL AND u0.NationalCode = t.NationalCode) + OR (t.UserIdHint IS NULL AND EXISTS ( + SELECT 1 FROM CMS.DayaLoanContracts dx + WHERE dx.UserId = u0.Id + AND dx.NationalCode = t.NationalCode + AND dx.IsDeleted = 0 + )) + ) + ORDER BY + CASE WHEN t.UserIdHint IS NOT NULL AND u0.Id = t.UserIdHint THEN 0 ELSE 1 END, + CASE WHEN u0.NationalCode = t.NationalCode THEN 0 ELSE 1 END, + u0.Id +) u +OUTER APPLY ( + SELECT TOP (1) d0.* + FROM CMS.DayaLoanContracts d0 + WHERE d0.UserId = u.Id + AND ( + d0.ContractNumber = t.ContractNumber + OR d0.NationalCode = t.NationalCode + OR d0.IsDeleted IN (0, 1) + ) + ORDER BY + CASE WHEN d0.ContractNumber = t.ContractNumber THEN 0 ELSE 1 END, + CASE WHEN d0.IsDeleted = 0 THEN 0 ELSE 1 END, + d0.Id DESC +) d +OUTER APPLY ( + -- تراکنش دایا که از قبل شارژ شده (مرجع کیف پول یا RefId قرارداد) + SELECT TOP (1) t0.Id AS TxId + FROM CMS.Transactions t0 + WHERE t0.IsDeleted = 0 + AND t0.PaymentStatus = 0 + AND ( + t0.RefId = t.ContractNumber + OR ( + t0.Description LIKE N'%اعتبار دایا%' + AND EXISTS ( + SELECT 1 + FROM CMS.UserWallets w + INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id + WHERE w.UserId = u.Id + AND h.RefrenceId = t0.Id + ) + ) + ) + ORDER BY + CASE WHEN t0.RefId = t.ContractNumber THEN 0 ELSE 1 END, + t0.Id DESC +) etx; + +PRINT '=== Before ==='; +SELECT Verdict, COUNT(*) AS Cnt FROM @Work GROUP BY Verdict; +SELECT ContractNumber, NationalCode, UserId, DayaContractId, HasCredit, IsProcessed, HasDayaUpp, + ExistingDayaTxId, Verdict +FROM @Work +ORDER BY Verdict, ContractNumber; + +BEGIN TRAN; + +DECLARE @ContractNumber NVARCHAR(50), @NationalCode NVARCHAR(20), @UserId BIGINT; +DECLARE @DayaContractId BIGINT, @HasCredit BIT, @IsProcessed BIT, @HasDayaUpp BIT, @Verdict NVARCHAR(20); +DECLARE @ExistingDayaTxId BIGINT; +DECLARE @TxId BIGINT, @WalletId BIGINT, @Action NVARCHAR(200); + +DECLARE @Log TABLE ( + ContractNumber NVARCHAR(50), + UserId BIGINT, + VerdictBefore NVARCHAR(20), + ActionTaken NVARCHAR(200), + TxId BIGINT NULL +); + +DECLARE cur CURSOR LOCAL FAST_FORWARD FOR + SELECT ContractNumber, NationalCode, UserId, DayaContractId, HasCredit, IsProcessed, HasDayaUpp, ExistingDayaTxId, Verdict + FROM @Work + WHERE Verdict IN (N'NOT_PROCESSED', N'PARTIAL'); + +OPEN cur; +FETCH NEXT FROM cur INTO @ContractNumber, @NationalCode, @UserId, @DayaContractId, @HasCredit, @IsProcessed, @HasDayaUpp, @ExistingDayaTxId, @Verdict; + +WHILE @@FETCH_STATUS = 0 +BEGIN + SET @TxId = NULL; + SET @Action = N''; + + IF @UserId IS NULL + BEGIN + INSERT INTO @Log VALUES (@ContractNumber, NULL, @Verdict, N'SKIP NO_USER', NULL); + GOTO NextRow; + END + + ------------------------------------------------------------ + -- Ensure / revive DayaLoanContract + ------------------------------------------------------------ + IF @DayaContractId IS NULL + BEGIN + INSERT INTO CMS.DayaLoanContracts + ( + UserId, NationalCode, ContractNumber, Status, IsProcessed, + LastCheckDate, ProcessedDate, TransactionId, + Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted + ) + VALUES + ( + @UserId, @NationalCode, @ContractNumber, 2, 0, + @Now, NULL, NULL, + @Now, NULL, @Now, NULL, 0 + ); + SET @DayaContractId = SCOPE_IDENTITY(); + SET @Action = @Action + N'INSERT_CONTRACT;'; + END + ELSE + BEGIN + UPDATE CMS.DayaLoanContracts + SET + IsDeleted = 0, + NationalCode = @NationalCode, + ContractNumber = @ContractNumber, + Status = 2, -- Received («فعال شده») + LastCheckDate = @Now, + LastModified = @Now + WHERE Id = @DayaContractId; + SET @Action = @Action + N'UPDATE_CONTRACT;'; + END + + ------------------------------------------------------------ + -- اگر تراکنش/شارژ «اعتبار دایا» از قبل هست → همان Tx را بگیر؛ شارژ نکن + ------------------------------------------------------------ + IF @ExistingDayaTxId IS NOT NULL + BEGIN + SET @TxId = @ExistingDayaTxId; + SET @Action = @Action + CONCAT(N'REUSE_EXISTING_DAYA_TX=', @TxId, N';'); + + UPDATE CMS.Users + SET + PackagePurchaseMethod = 1, + HasReceivedDayaCredit = 1, + DayaCreditReceivedAt = COALESCE(DayaCreditReceivedAt, @Now), + LastModified = @Now + WHERE Id = @UserId; + + SET @HasCredit = 1; + SET @Action = @Action + N'SET_USER_FLAGS(no charge);'; + END + ELSE IF @HasCredit = 0 + BEGIN + -- Full wallet charge only if never credited and no existing Daya tx + INSERT INTO CMS.Transactions + ( + Amount, Description, PaymentStatus, PaymentDate, RefId, Type, + Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted + ) + VALUES + ( + @PackagePrice, + CONCAT(N'دریافت اعتبار دایا - قرارداد ', @ContractNumber), + 0, -- Success + @Now, + @ContractNumber, + 2, -- DepositExternal1 + @Now, NULL, @Now, NULL, 0 + ); + SET @TxId = SCOPE_IDENTITY(); + + SELECT @WalletId = Id FROM CMS.UserWallets WHERE UserId = @UserId; + IF @WalletId IS NULL + BEGIN + INSERT INTO CMS.UserWallets (UserId, Balance, NetworkBalance, DiscountBalance, WalletMode, Created, LastModified, IsDeleted) + VALUES (@UserId, 0, 0, 0, 0, @Now, @Now, 0); + SET @WalletId = SCOPE_IDENTITY(); + END + + UPDATE CMS.UserWallets + SET + Balance = Balance + @PackagePrice, + DiscountBalance = DiscountBalance + @DiscountAmount, + LastModified = @Now + WHERE Id = @WalletId; + + INSERT INTO CMS.UserWalletHistories + ( + WalletId, CurrentBalance, ChangeValue, CurrentNetworkBalance, ChangeNerworkValue, + CurrentDiscountBalance, ChangeDiscountValue, IsIncrease, RefrenceId, PackageId, + Created, LastModified, IsDeleted + ) + VALUES + ( + @WalletId, 0, @PackagePrice, 0, 0, + 0, 0, 1, @TxId, @PackageId, + @Now, @Now, 0 + ), + ( + @WalletId, 0, 0, 0, 0, + 0, @DiscountAmount, 1, @TxId, @PackageId, + @Now, @Now, 0 + ); + + UPDATE CMS.Users + SET + HasReceivedDayaCredit = 1, + DayaCreditReceivedAt = @Now, + PackagePurchaseMethod = 1, -- DayaLoan + LastModified = @Now + WHERE Id = @UserId; + + SET @HasCredit = 1; + SET @Action = @Action + N'CHARGE_WALLET+USER_FLAGS;'; + END + ELSE + BEGIN + -- HasReceivedDayaCredit=1 ولی ExistingDayaTx پیدا نشد + UPDATE CMS.Users + SET + PackagePurchaseMethod = 1, + HasReceivedDayaCredit = 1, + DayaCreditReceivedAt = COALESCE(DayaCreditReceivedAt, @Now), + LastModified = @Now + WHERE Id = @UserId; + SET @Action = @Action + N'SKIP_CHARGE(HasReceivedDayaCredit);'; + END + + ------------------------------------------------------------ + -- Ensure successful Daya UPP (no duplicate if already exists) + ------------------------------------------------------------ + IF @HasDayaUpp = 0 + BEGIN + IF @TxId IS NULL + BEGIN + -- فقط لجر؛ بدون شارژ کیف (نباید برای کسی که Tx دایا دارد به اینجا برسد) + INSERT INTO CMS.Transactions + ( + Amount, Description, PaymentStatus, PaymentDate, RefId, Type, + Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted + ) + VALUES + ( + @PackagePrice, + CONCAT(N'لجر UPP دایا (بدون شارژ مجدد) - قرارداد ', @ContractNumber), + 0, + @Now, + CONCAT(N'UPP-DAYA-FIX-', @ContractNumber), + 2, + @Now, NULL, @Now, NULL, 0 + ); + SET @TxId = SCOPE_IDENTITY(); + SET @Action = @Action + N'LEDGER_TX;'; + END + + INSERT INTO CMS.UserPackagePurchases + ( + UserId, PackageId, PurchaseMethod, PurchasedAt, Amount, + OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted + ) + VALUES + ( + @UserId, @PackageId, 1, @Now, @PackagePrice, + NULL, @TxId, @Now, NULL, @Now, NULL, 0 + ); + SET @Action = @Action + N'INSERT_UPP;'; + END + + ------------------------------------------------------------ + -- Mark contract processed + ------------------------------------------------------------ + UPDATE CMS.DayaLoanContracts + SET + IsProcessed = 1, + ProcessedDate = COALESCE(ProcessedDate, @Now), + TransactionId = COALESCE(TransactionId, @TxId), + Status = 2, + ContractNumber = @ContractNumber, + IsDeleted = 0, + LastModified = @Now + WHERE Id = @DayaContractId; + + SET @Action = @Action + N'MARK_PROCESSED;'; + + INSERT INTO @Log VALUES (@ContractNumber, @UserId, @Verdict, @Action, @TxId); + +NextRow: + FETCH NEXT FROM cur INTO @ContractNumber, @NationalCode, @UserId, @DayaContractId, @HasCredit, @IsProcessed, @HasDayaUpp, @ExistingDayaTxId, @Verdict; +END + +CLOSE cur; +DEALLOCATE cur; + +PRINT '=== Actions ==='; +SELECT * FROM @Log ORDER BY VerdictBefore, ContractNumber; + +-- Re-score after changes +PRINT '=== After (same targets) ==='; +SELECT + t.ContractNumber, + t.NationalCode, + u.Id AS UserId, + u.HasReceivedDayaCredit, + d.IsProcessed, + d.ContractNumber AS DbContractNumber, + d.Status, + CASE WHEN EXISTS ( + SELECT 1 FROM CMS.UserPackagePurchases upp + LEFT JOIN CMS.Transactions tx ON tx.Id = upp.TransactionId AND tx.IsDeleted = 0 + WHERE upp.UserId = u.Id AND upp.IsDeleted = 0 AND upp.PurchaseMethod = 1 + AND (upp.TransactionId IS NULL OR tx.PaymentStatus = 0) + ) THEN 1 ELSE 0 END AS HasDayaUpp, + CASE + WHEN u.Id IS NULL THEN N'NO_USER' + WHEN u.HasReceivedDayaCredit = 1 AND d.IsProcessed = 1 + AND EXISTS ( + SELECT 1 FROM CMS.UserPackagePurchases upp + LEFT JOIN CMS.Transactions tx ON tx.Id = upp.TransactionId AND tx.IsDeleted = 0 + WHERE upp.UserId = u.Id AND upp.IsDeleted = 0 AND upp.PurchaseMethod = 1 + AND (upp.TransactionId IS NULL OR tx.PaymentStatus = 0) + ) THEN N'DONE' + WHEN u.HasReceivedDayaCredit = 1 OR d.IsProcessed = 1 THEN N'PARTIAL' + ELSE N'NOT_PROCESSED' + END AS Verdict +FROM @Targets t +LEFT JOIN CMS.Users u ON u.IsDeleted = 0 AND ( + (t.UserIdHint IS NOT NULL AND u.Id = t.UserIdHint) OR (t.UserIdHint IS NULL AND u.NationalCode = t.NationalCode) +) +LEFT JOIN CMS.DayaLoanContracts d ON d.IsDeleted = 0 AND d.UserId = u.Id + AND (d.ContractNumber = t.ContractNumber OR d.NationalCode = t.NationalCode) +ORDER BY Verdict, t.ContractNumber; + +IF @Commit = 1 +BEGIN + COMMIT; + PRINT 'COMMITTED.'; +END +ELSE +BEGIN + ROLLBACK; + PRINT 'Dry-run ROLLBACK. Set @Commit = 1 to apply.'; +END diff --git a/src/dbbkup/Fix_WrongDaya_ToManual_PurchaseMethod.sql b/src/dbbkup/Fix_WrongDaya_ToManual_PurchaseMethod.sql new file mode 100644 index 0000000..5827691 --- /dev/null +++ b/src/dbbkup/Fix_WrongDaya_ToManual_PurchaseMethod.sql @@ -0,0 +1,157 @@ +/* + Correct users wrongly marked as DayaLoan — should be Manual. + + Affects: + - CMS.Users.PackagePurchaseMethod (1 → 3) + - CMS.ClubMemberships.PurchaseMethod (1 or 0 → 3) + - CMS.ClubMembershipCycles.PurchaseMethod (1 or 0 → 3) + - CMS.UserPackagePurchases.PurchaseMethod (1 → 3) + - CMS.Users.HasReceivedDayaCredit / DayaCreditReceivedAt clear + - CMS.DayaLoanContracts soft-delete stub rows + (IsProcessed=0, no TransactionId, no ProcessedDate) + for these UserIds only + + PackagePurchaseMethod: None=0, DayaLoan=1, DirectPurchase=2, Manual=3 + + NOTE: UserId=43 shares NationalCode 5289780335 with UserId=42 (real Excel Daya). + This script only touches UserId=43's own rows — not UserId=42's contract. + + Defaults to ROLLBACK. Uncomment COMMIT after verifying PRINT counts. +*/ + +SET NOCOUNT ON; +SET XACT_ABORT ON; + +BEGIN TRAN; + +DECLARE @FixUsers TABLE (UserId BIGINT PRIMARY KEY); +INSERT INTO @FixUsers (UserId) VALUES +(2),(7),(8),(9),(10),(11),(12),(13),(40),(41),(43),(45),(47), +(52),(58),(69),(71),(72),(74),(75),(87),(88),(89),(90),(91),(93), +(96),(99),(103),(105),(106),(111),(113),(115),(116),(119),(121), +(125),(130),(142),(162),(169),(170),(171),(172),(175),(176); + +------------------------------------------------------------ +-- 1) Users +------------------------------------------------------------ +UPDATE u +SET + u.PackagePurchaseMethod = 3, -- Manual + u.HasReceivedDayaCredit = 0, + u.DayaCreditReceivedAt = NULL, + u.LastModified = SYSUTCDATETIME() +FROM CMS.Users u +INNER JOIN @FixUsers f ON f.UserId = u.Id +WHERE u.IsDeleted = 0 + AND ( + u.PackagePurchaseMethod = 1 -- DayaLoan + OR u.HasReceivedDayaCredit = 1 + OR u.DayaCreditReceivedAt IS NOT NULL + ); + +PRINT CONCAT('Users updated: ', @@ROWCOUNT); + +------------------------------------------------------------ +-- 2) ClubMemberships +------------------------------------------------------------ +UPDATE m +SET + m.PurchaseMethod = 3, -- Manual + m.LastModified = SYSUTCDATETIME() +FROM CMS.ClubMemberships m +INNER JOIN @FixUsers f ON f.UserId = m.UserId +WHERE m.IsDeleted = 0 + AND m.PurchaseMethod IN (0, 1); -- None or DayaLoan + +PRINT CONCAT('ClubMemberships updated: ', @@ROWCOUNT); + +------------------------------------------------------------ +-- 3) ClubMembershipCycles +------------------------------------------------------------ +UPDATE c +SET + c.PurchaseMethod = 3, -- Manual + c.LastModified = SYSUTCDATETIME() +FROM CMS.ClubMembershipCycles c +INNER JOIN @FixUsers f ON f.UserId = c.UserId +WHERE c.IsDeleted = 0 + AND c.PurchaseMethod IN (0, 1); + +PRINT CONCAT('ClubMembershipCycles updated: ', @@ROWCOUNT); + +------------------------------------------------------------ +-- 4) UserPackagePurchases (incl. backfill rows) +------------------------------------------------------------ +UPDATE upp +SET + upp.PurchaseMethod = 3, -- Manual + upp.LastModified = SYSUTCDATETIME() +FROM CMS.UserPackagePurchases upp +INNER JOIN @FixUsers f ON f.UserId = upp.UserId +WHERE upp.IsDeleted = 0 + AND upp.PurchaseMethod = 1; -- only Daya → Manual (keep Direct=2 if any) + +PRINT CONCAT('UserPackagePurchases updated: ', @@ROWCOUNT); + +------------------------------------------------------------ +-- 5) Soft-delete stub DayaLoanContracts (never processed) +------------------------------------------------------------ +UPDATE d +SET + d.IsDeleted = 1, + d.LastModified = SYSUTCDATETIME() +FROM CMS.DayaLoanContracts d +INNER JOIN @FixUsers f ON f.UserId = d.UserId +WHERE d.IsDeleted = 0 + AND d.IsProcessed = 0 + AND d.TransactionId IS NULL + AND d.ProcessedDate IS NULL; + +PRINT CONCAT('DayaLoanContracts soft-deleted (stubs): ', @@ROWCOUNT); + +------------------------------------------------------------ +-- Verify +------------------------------------------------------------ +SELECT 'Users still Daya' AS CheckName, COUNT(*) AS Cnt +FROM CMS.Users u +INNER JOIN @FixUsers f ON f.UserId = u.Id +WHERE u.IsDeleted = 0 AND u.PackagePurchaseMethod = 1 +UNION ALL +SELECT 'Club still Daya/None', COUNT(*) +FROM CMS.ClubMemberships m +INNER JOIN @FixUsers f ON f.UserId = m.UserId +WHERE m.IsDeleted = 0 AND m.PurchaseMethod IN (0, 1) +UNION ALL +SELECT 'Cycle still Daya/None', COUNT(*) +FROM CMS.ClubMembershipCycles c +INNER JOIN @FixUsers f ON f.UserId = c.UserId +WHERE c.IsDeleted = 0 AND c.PurchaseMethod IN (0, 1) +UNION ALL +SELECT 'UPP still Daya', COUNT(*) +FROM CMS.UserPackagePurchases upp +INNER JOIN @FixUsers f ON f.UserId = upp.UserId +WHERE upp.IsDeleted = 0 AND upp.PurchaseMethod = 1 +UNION ALL +SELECT 'Active stub Daya contracts', COUNT(*) +FROM CMS.DayaLoanContracts d +INNER JOIN @FixUsers f ON f.UserId = d.UserId +WHERE d.IsDeleted = 0 + AND d.IsProcessed = 0 + AND d.TransactionId IS NULL + AND d.ProcessedDate IS NULL; + +-- Sample after +SELECT u.Id AS UserId, + u.FirstName, u.LastName, u.Mobile, + u.PackagePurchaseMethod, + m.PurchaseMethod AS ClubPurchaseMethod +FROM CMS.Users u +INNER JOIN @FixUsers f ON f.UserId = u.Id +LEFT JOIN CMS.ClubMemberships m ON m.UserId = u.Id AND m.IsDeleted = 0 +WHERE u.IsDeleted = 0 +ORDER BY u.Id; + +-- Uncomment to apply: +-- COMMIT; +ROLLBACK; +PRINT 'Rolled back (safety). Uncomment COMMIT and remove ROLLBACK to apply.'; diff --git a/src/dbbkup/Rollback_Backfill_UserPackagePurchases.sql b/src/dbbkup/Rollback_Backfill_UserPackagePurchases.sql new file mode 100644 index 0000000..44bab1e --- /dev/null +++ b/src/dbbkup/Rollback_Backfill_UserPackagePurchases.sql @@ -0,0 +1,116 @@ +/* + Rollback for Backfill_UserPackagePurchases_FromClubMemberships.sql + + What it undoes: + A) 77 synthetic rows: Tx with RefId LIKE 'UPP-BACKFILL-%' + their UPP + B) 24 ManualPayment-linked UPP rows inserted by backfill + (same TransactionId as Approved ManualPayments — NOT deleting those Transactions) + + BEFORE RUN: + Set @BackfillStartedAt to a timestamp slightly BEFORE you ran the backfill + (e.g. if you ran at 06:40, use '2026-08-14 06:00:00'). + Part B only deletes UPP with Created >= that time so later real CreateManualPayment rows stay. + + Safe to preview: leave @Commit = 0 (default) to only PRINT counts inside a rolled-back tran. + Set @Commit = 1 to actually apply. +*/ + +SET NOCOUNT ON; +SET XACT_ABORT ON; + +DECLARE @BackfillStartedAt DATETIME2 = '2026-08-14 00:00:00'; -- << adjust +DECLARE @Commit BIT = 0; -- 0 = dry-run (rollback), 1 = commit deletes + +BEGIN TRAN; + +------------------------------------------------------------ +-- A) Synthetic Tx + UPP (RefId marker) +------------------------------------------------------------ +DECLARE @SyntheticUpp TABLE (Id BIGINT PRIMARY KEY, TransactionId BIGINT); + +INSERT INTO @SyntheticUpp (Id, TransactionId) +SELECT upp.Id, upp.TransactionId +FROM CMS.UserPackagePurchases upp +INNER JOIN CMS.Transactions t ON t.Id = upp.TransactionId +WHERE t.RefId LIKE N'UPP-BACKFILL-%'; + +DECLARE @SynthUppCount INT = (SELECT COUNT(*) FROM @SyntheticUpp); +DECLARE @SynthTxCount INT = +( + SELECT COUNT(*) + FROM CMS.Transactions t + WHERE t.RefId LIKE N'UPP-BACKFILL-%' +); + +DELETE upp +FROM CMS.UserPackagePurchases upp +INNER JOIN @SyntheticUpp s ON s.Id = upp.Id; + +DELETE t +FROM CMS.Transactions t +WHERE t.RefId LIKE N'UPP-BACKFILL-%'; + +PRINT CONCAT('Deleted synthetic UPP: ', @SynthUppCount); +PRINT CONCAT('Deleted synthetic Transactions: ', @SynthTxCount); + +------------------------------------------------------------ +-- B) UPP created from ManualPayments reuse (no new Tx) +------------------------------------------------------------ +DECLARE @ManualUpp TABLE (Id BIGINT PRIMARY KEY); + +INSERT INTO @ManualUpp (Id) +SELECT upp.Id +FROM CMS.UserPackagePurchases upp +INNER JOIN CMS.ManualPayments mp + ON mp.TransactionId = upp.TransactionId + AND mp.IsDeleted = 0 + AND mp.Status = 1 -- Approved +WHERE upp.IsDeleted = 0 + AND upp.PurchaseMethod = 3 -- Manual + AND upp.Created >= @BackfillStartedAt + -- exclude anything already covered as synthetic (shouldn't overlap) + AND NOT EXISTS ( + SELECT 1 FROM CMS.Transactions t + WHERE t.Id = upp.TransactionId AND t.RefId LIKE N'UPP-BACKFILL-%' + ); + +DECLARE @ManualUppCount INT = (SELECT COUNT(*) FROM @ManualUpp); + +DELETE upp +FROM CMS.UserPackagePurchases upp +INNER JOIN @ManualUpp m ON m.Id = upp.Id; + +PRINT CONCAT('Deleted ManualPayment-linked UPP (Created >= @BackfillStartedAt): ', @ManualUppCount); +PRINT CONCAT('@BackfillStartedAt used: ', CONVERT(varchar(30), @BackfillStartedAt, 126)); + +------------------------------------------------------------ +-- Optional sanity after rollback +------------------------------------------------------------ +DECLARE @Orphans INT = +( + SELECT COUNT(*) + FROM CMS.ClubMemberships m + WHERE m.IsDeleted = 0 + AND NOT EXISTS ( + SELECT 1 + FROM CMS.UserPackagePurchases upp + LEFT JOIN CMS.Transactions t ON t.Id = upp.TransactionId AND t.IsDeleted = 0 + WHERE upp.UserId = m.UserId + AND upp.IsDeleted = 0 + AND (upp.TransactionId IS NULL OR t.PaymentStatus = 0) + ) +); + +PRINT CONCAT('Club members without successful UPP after rollback: ', @Orphans); +PRINT '(Expect around 101 if dump-era state, if no new purchases since.)'; + +IF @Commit = 1 +BEGIN + COMMIT; + PRINT 'Rollback COMMITTED.'; +END +ELSE +BEGIN + ROLLBACK; + PRINT 'Dry-run only — ROLLBACK. Set @Commit = 1 after verifying counts.'; +END diff --git a/src/dbbkup/Update_DayaLoanContracts_ContractNumbers.sql b/src/dbbkup/Update_DayaLoanContracts_ContractNumbers.sql new file mode 100644 index 0000000..dde8dd4 --- /dev/null +++ b/src/dbbkup/Update_DayaLoanContracts_ContractNumbers.sql @@ -0,0 +1,145 @@ +/* + Update DayaLoanContracts.ContractNumber from MerchantCreditList. + + Match: NationalCode (unique cases). + Special case — same NationalCode 2490371436 on TWO users (two mobiles): + UserId=50 (09207316560, Created 2025-11-11) → C4_D8815963 (فعال‌سازی 1404/8/24 = 2025-11-15) + UserId=51 (09366426797, Created 2025-11-12) → C4_G8945289 (فعال‌سازی 1404/8/30 = 2025-11-21) + User 51 may have no DayaLoanContracts row yet → INSERT. + + Script defaults to ROLLBACK; uncomment COMMIT after verifying. +*/ + +SET NOCOUNT ON; +SET XACT_ABORT ON; + +BEGIN TRAN; + +------------------------------------------------------------ +-- 1) Unique NationalCodes → UPDATE ContractNumber +------------------------------------------------------------ +;WITH Src AS ( + SELECT * + FROM (VALUES + (N'C4_C11231802', N'0312980973'), + (N'C4_H10706299', N'4060667961'), + (N'C4_E10490523', N'4072306754'), + (N'C4_C10337736', N'0054140161'), + (N'C4_Q10727431', N'5279791253'), + (N'C4_W10568862', N'1960144804'), + (N'C4_V10338780', N'4072240567'), + (N'C4_G10538762', N'6639906902'), + (N'C4_N10921338', N'0943060761'), + (N'C4_P9320383', N'2572729636'), + (N'C4_Y9276146', N'0012011061'), + (N'C4_Q9465813', N'2480017796'), + (N'C4_A9107753', N'0450314103'), + (N'C4_B9348378', N'0452314488'), + (N'C4_U9595222', N'2062876815'), + (N'C4_X9630520', N'0680164286'), + (N'C4_J9199624', N'0681699401'), + (N'C4_B9813553', N'0055680364'), + (N'C4_M9994735', N'0793851572'), + (N'C4_S9301452', N'0082242925'), + (N'C4_V9257168', N'4459676451'), + (N'C4_T9961063', N'0312342993'), + (N'C4_L9524515', N'0024757489'), + (N'C4_Q9947535', N'2480259668'), + (N'C4_T9892089', N'2451477016'), + (N'C4_N9945163', N'1289603669'), + (N'C4_W8785358', N'0322922720'), + (N'C4_J8388494', N'2002637903'), + (N'C4_V8165679', N'0081093829'), + (N'C4_T8579002', N'5289780335') + -- 2490371436 handled separately by UserId below + ) AS v(ContractNumber, NationalCode) +) +UPDATE d +SET + d.ContractNumber = s.ContractNumber, + d.LastModified = SYSUTCDATETIME() +FROM CMS.DayaLoanContracts d +INNER JOIN Src s ON s.NationalCode = d.NationalCode +WHERE d.IsDeleted = 0; + +PRINT CONCAT('Updated by NationalCode (unique): ', @@ROWCOUNT); + +------------------------------------------------------------ +-- 2) Shared NC 2490371436 — match by UserId + Excel activation date +------------------------------------------------------------ +-- UserId 50 ← earlier activation 1404/8/24 → C4_D8815963 +UPDATE CMS.DayaLoanContracts +SET + ContractNumber = N'C4_D8815963', + LastModified = SYSUTCDATETIME() +WHERE IsDeleted = 0 + AND UserId = 50 + AND NationalCode = N'2490371436'; + +PRINT CONCAT('Updated UserId=50 (C4_D8815963): ', @@ROWCOUNT); + +-- UserId 51 ← later activation 1404/8/30 → C4_G8945289 +IF EXISTS ( + SELECT 1 FROM CMS.DayaLoanContracts + WHERE IsDeleted = 0 AND UserId = 51 AND NationalCode = N'2490371436' +) +BEGIN + UPDATE CMS.DayaLoanContracts + SET + ContractNumber = N'C4_G8945289', + LastModified = SYSUTCDATETIME() + WHERE IsDeleted = 0 + AND UserId = 51 + AND NationalCode = N'2490371436'; + + PRINT CONCAT('Updated UserId=51 (C4_G8945289): ', @@ROWCOUNT); +END +ELSE +BEGIN + INSERT INTO CMS.DayaLoanContracts + ( + UserId, NationalCode, ContractNumber, Status, IsProcessed, + LastCheckDate, ProcessedDate, TransactionId, + Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted + ) + VALUES + ( + 51, + N'2490371436', + N'C4_G8945289', + 0, -- NotRequested (same as sibling row for UserId=50 in dump; adjust if needed) + 0, + NULL, + NULL, + NULL, + SYSUTCDATETIME(), + NULL, + SYSUTCDATETIME(), + NULL, + 0 + ); + + PRINT 'Inserted DayaLoanContracts for UserId=51 (C4_G8945289).'; +END + +------------------------------------------------------------ +-- Verify +------------------------------------------------------------ +SELECT + d.Id, + d.UserId, + u.Mobile, + d.NationalCode, + d.ContractNumber, + d.Status, + d.IsProcessed +FROM CMS.DayaLoanContracts d +INNER JOIN CMS.Users u ON u.Id = d.UserId +WHERE d.IsDeleted = 0 + AND d.NationalCode = N'2490371436' +ORDER BY d.UserId; + +-- Uncomment to apply: +-- COMMIT; +ROLLBACK; +PRINT 'Rolled back (safety). Uncomment COMMIT and remove ROLLBACK to apply.'; From 2d23dbc7989b950e5af856b626ead4418effb160 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sun, 23 Aug 2026 23:01:17 +0330 Subject: [PATCH 3/3] feat(withdrawal): normalize IBAN format in withdrawal requests - Added IbanNormalizer to validate and normalize IBAN numbers in RequestWithdrawalCommandHandler and UserWalletService. - Implemented error handling for invalid IBAN formats, ensuring compliance with expected standards. - Updated relevant methods to handle normalized IBANs for cash withdrawal requests. --- .../RequestWithdrawalCommandHandler.cs | 7 +++- .../Common/IbanNormalizer.cs | 34 +++++++++++++++++++ .../Services/UserWalletService.cs | 14 +++++++- 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/CMSMicroservice.Application/Common/IbanNormalizer.cs diff --git a/src/CMSMicroservice.Application/CommissionCQ/Commands/RequestWithdrawal/RequestWithdrawalCommandHandler.cs b/src/CMSMicroservice.Application/CommissionCQ/Commands/RequestWithdrawal/RequestWithdrawalCommandHandler.cs index 28894d2..3f04084 100644 --- a/src/CMSMicroservice.Application/CommissionCQ/Commands/RequestWithdrawal/RequestWithdrawalCommandHandler.cs +++ b/src/CMSMicroservice.Application/CommissionCQ/Commands/RequestWithdrawal/RequestWithdrawalCommandHandler.cs @@ -1,3 +1,5 @@ +using CMSMicroservice.Application.Common; + namespace CMSMicroservice.Application.CommissionCQ.Commands.RequestWithdrawal; public class RequestWithdrawalCommandHandler : IRequestHandler @@ -37,7 +39,10 @@ public class RequestWithdrawalCommandHandler : IRequestHandler +/// نرمال‌سازی و اعتبارسنجی شماره شبا ایران (IR + 24 رقم). +/// +public static class IbanNormalizer +{ + private static readonly Regex IranianIbanRegex = new(@"^IR\d{24}$", RegexOptions.Compiled); + + /// + /// فاصله و خط تیره را حذف می‌کند، به حروف بزرگ تبدیل می‌کند و یک پیشوند IR می‌گذارد. + /// در صورت نامعتبر بودن، null برمی‌گرداند. + /// + public static string? TryNormalize(string? iban) + { + if (string.IsNullOrWhiteSpace(iban)) + return null; + + var normalized = iban.Trim().ToUpperInvariant() + .Replace(" ", "", StringComparison.Ordinal) + .Replace("-", "", StringComparison.Ordinal); + + if (normalized.StartsWith("IR", StringComparison.Ordinal)) + normalized = normalized[2..]; + + normalized = "IR" + normalized; + + return IranianIbanRegex.IsMatch(normalized) ? normalized : null; + } + + public static bool IsValid(string? iban) => TryNormalize(iban) is not null; +} diff --git a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs index fa60522..8c92e58 100644 --- a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs +++ b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs @@ -18,6 +18,7 @@ using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawals; using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawalSettings; using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Application.Common; using CMSMicroservice.Application.Common.Exceptions; using CMSMicroservice.Domain.Common; using CMSMicroservice.Domain.Enums; @@ -175,7 +176,18 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase // Update payout with withdrawal info payout.WithdrawalMethod = (Domain.Enums.WithdrawalMethod)request.WithdrawalMethod; - payout.IbanNumber = request.IbanNumber; + if (payout.WithdrawalMethod == Domain.Enums.WithdrawalMethod.Cash) + { + var normalizedIban = IbanNormalizer.TryNormalize(request.IbanNumber); + if (normalizedIban is null) + throw new RpcException(new Status(StatusCode.InvalidArgument, + "فرمت شماره شبا معتبر نیست. باید IR و ۲۴ رقم باشد.")); + payout.IbanNumber = normalizedIban; + } + else + { + payout.IbanNumber = null; + } payout.Status = Domain.Enums.CommissionPayoutStatus.WithdrawRequested; await _context.SaveChangesAsync(context.CancellationToken);