/* 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.';