68 Commits

Author SHA1 Message Date
masoodafar-web 053c5a7c4f Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 17m27s
2026-08-07 14:38:07 +03:30
masoodafar-web 82944e11d0 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-07-26 00:52:02 +03:30
masoodafar-web bb20ad2c4e Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-06-30 02:28:32 +03:30
masoodafar-web 99aab3beff Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 19m40s
2026-06-26 19:30:03 +03:30
masoodafar-web 19be5dc9dc Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 16m51s
2026-06-20 23:45:47 +03:30
m-moghaddam 5c1e3cf601 Merge remote-tracking branch 'origin/kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 19m16s
2026-06-20 17:28:19 +03:30
masoodafar-web d82b7c28a8 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 20m50s
2026-06-19 02:37:34 +03:30
masoodafar-web 3df738e496 Merge branches 'kub-stage' and 'production' of https://git.se.kbs1.ir/admin/CMS into production
Build and Deploy to Production / build-and-deploy (push) Successful in 11m1s
2026-06-08 22:47:48 +03:30
masoodafar-web 3a9c3438e6 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 12m7s
Deploy DiscountOrderService build fix.
2026-06-08 02:30:01 +03:30
masoodafar-web 12ebdc9e87 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
Deploy blog slug auto-generation fix to production.
2026-06-08 02:06:50 +03:30
masoodafar-web 9f1bc2bcdb Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 15m25s
Deploy payment idempotency fixes and per-user payment locks to production.
2026-06-08 02:00:30 +03:30
masoodafar-web 354ba1dd1b Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 8m37s
2026-05-29 19:59:05 +03:30
masoodafar-web 3b9958a700 fix(ZarinpalReconciliationJob): correct typo in logging method for unverified list
Build and Deploy to Production / build-and-deploy (push) Successful in 19m55s
2026-05-21 20:20:38 +03:30
masoodafar-web 9c562b12c7 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-05-21 20:19:43 +03:30
masoodafar-web 3c9957463d Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 14m52s
2026-05-21 19:27:56 +03:30
masoodafar-web 4df71107a9 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 12m46s
2026-05-21 18:27:04 +03:30
masoodafar-web a7fc959d3e Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 14m3s
2026-05-18 01:56:29 +03:30
masoodafar-web d96558aa4b Revert "feat(commission): add BulkCreditPayouts — credit wallets for pending payouts"
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
This reverts commit a2d13c72c3.
2026-05-18 00:38:40 +03:30
masoodafar-web a2d13c72c3 feat(commission): add BulkCreditPayouts — credit wallets for pending payouts
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
- commission.proto: add BulkCreditPayouts rpc + request/response messages; bump version to 0.0.198
- BulkCreditPayoutsCommand + Handler: load all Pending payouts for given week, credit UserWallet.Balance for each user, create UserWalletHistory + CommissionPayoutHistory records, set Status=Paid + PaidAt
- CommissionService: wire up BulkCreditPayouts gRPC method
- CommissionProfile: add Mapster mappings for request/result ↔ proto types

Previously ProcessUserPayouts created records with Status=Pending but never credited wallets, making CustomerWithdrawBalance unreachable (it requires Status==Paid).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 00:36:05 +03:30
masoodafar-web 4e8caa6e2c Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 19m44s
2026-05-18 00:12:13 +03:30
masoodafar-web 37a017998c Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 9m24s
2026-05-14 01:13:03 +03:30
masoodafar-web 16acb8f3b5 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 7m7s
2026-05-10 21:37:51 +03:30
masoodafar-web 34d2354fd4 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 9m37s
2026-05-05 01:54:44 +03:30
masoodafar-web be2b22c50a Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 10m25s
2026-05-04 23:55:16 +03:30
masoodafar-web b260fd56fa Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 10m7s
2026-05-04 00:04:36 +03:30
masoodafar-web 1e3ba03c5c Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 8m34s
2026-05-01 00:56:21 +03:30
masoodafar-web ee7d29015f Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-05-01 00:47:44 +03:30
masoodafar-web b053d86006 feat: Add GetNetworkTree stored procedure for binary network retrieval with activation logic
Build and Deploy to Production / build-and-deploy (push) Successful in 8m27s
2026-05-01 00:27:44 +03:30
masoodafar-web 15044c66cb Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-05-01 00:15:48 +03:30
masoodafar-web 0983cdf88f Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 8m56s
2026-04-30 04:27:43 +03:30
masoodafar-web b8120dd1b9 config: add master OTP code to production appsettings 2026-04-29 00:49:55 +03:30
masoodafar-web 7ce5539793 feat: add master OTP bypass for login + referral code field support 2026-04-29 00:49:33 +03:30
masoodafar-web 9a4f1e6401 chore: merge kub-stage into production
Build and Deploy to Production / build-and-deploy (push) Successful in 8m11s
2026-04-28 23:28:44 +03:30
masoodafar-web 091a04f3aa Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 24m9s
2026-03-15 03:19:41 +03:30
masoodafar-web 62a0f8594d fix: Update Kavenegar API key and modify database connection string
Build and Deploy to Production / build-and-deploy (push) Successful in 8m39s
2026-02-28 05:03:38 +03:30
masoodafar-web fb81aff095 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 8m48s
# Conflicts:
#	.gitea/workflows/kub-deploy.yml
#	k8s/staging/cms-deployment.yaml
#	src/CMSMicroservice.Application/DiscountShopCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommandHandler.cs
#	src/CMSMicroservice.Application/DiscountShopCQ/Commands/PlaceOrder/PlaceOrderCommandHandler.cs
#	src/CMSMicroservice.Application/WalletCQ/Commands/VerifyDiscountWalletCharge/VerifyDiscountWalletChargeCommandHandler.cs
#	src/CMSMicroservice.Application/WalletCQ/Commands/VerifyMagicWalletCharge/VerifyMagicWalletChargeCommandHandler.cs
#	src/CMSMicroservice.Infrastructure/Services/Payment/ZarinPalPaymentService.cs
#	src/CMSMicroservice.WebApi/Controllers/PaymentCallbackController.cs
#	src/CMSMicroservice.WebApi/appsettings.Production.json
#	src/CMSMicroservice.WebApi/appsettings.Staging.json
#	src/CMSMicroservice.WebApi/appsettings.json
2026-02-28 04:51:57 +03:30
masoodafar-web 491b8a0d0f fix: remove double ×10 Rial conversion in ZarinPalPaymentService
Build and Deploy to Production / build-and-deploy (push) Successful in 10m10s
Root cause: FrontOffice already converts Toman→Rial (×10) before sending
to CMS. ZarinPalPaymentService was multiplying by 10 AGAIN, causing
amounts to be 10x too large.

Example: user enters 500K Toman → FO sends 5M Rial → CMS did ×10 → 50M
Rial sent to ZarinPal → showed 5M Toman instead of 500K.

Changes:
- ZarinPalPaymentService.InitiatePaymentAsync: remove ×10 (amount already Rial)
- ZarinPalPaymentService.VerifyPaymentWithAmountAsync: remove ×10 + accept Rial
- ZarinPalPaymentService.VerifyResult.Amount: return Rial (no /10 conversion)
- VerifyMagicWalletChargeCommandHandler: remove /10 before calling Verify
- PaymentCallbackController: update comments (amount is Rial)
- Also includes: improved HTTP error logging for non-200 responses
- Also includes: production URL fix (kbs1→kbs2)
2026-02-24 00:49:47 +03:30
masoodafar-web be80136ae8 fix: reduce discount order expiry from 30 to 15 minutes
Build and Deploy to Production / build-and-deploy (push) Successful in 9m8s
2026-02-24 00:24:30 +03:30
masoodafar-web 5e9984f0e5 fix: validate discount wallet balance before applying discount in PlaceOrderCommandHandler 2026-02-24 00:24:30 +03:30
masoodafar-web 1d07ae4f71 fix: add missing UserWalletChangeLog for discount shop purchases and discount wallet charge
Bug: when buying from discount shop, DiscountBalance was deducted from DB
but no UserWalletChangeLog was created — making it invisible in wallet history.
Same issue existed for discount wallet top-up (charge).

Fixed in 3 handlers:
- PlaceOrderCommandHandler (fully paid by discount balance path)
- CompleteOrderPaymentCommandHandler (gateway + discount balance path)
- VerifyDiscountWalletChargeCommandHandler (discount wallet charge)
2026-02-24 00:24:30 +03:30
masoodafar-web 775a9bd75d fix: update .gitignore to include environment-specific appsettings files 2026-02-24 00:18:42 +03:30
masoodafar-web 2620a24597 fix: correct production URLs from kbs1 to kbs2 in cms-config
CmsBaseUrl: cms.kbs1.ir → cms.kbs2.ir
FrontOfficeBaseUrl: kbs1.ir → kbs2.ir

This was causing ZarinPal callback to go to staging instead of production,
resulting in 401 errors (-1 unknown error).
2026-02-24 00:18:24 +03:30
masoodafar-web def3b94d89 fix: update appsettings for production environment — adjust URLs and connection strings
Build and Deploy to Production / build-and-deploy (push) Successful in 9m32s
2026-02-23 23:59:41 +03:30
masoodafar-web 3ebe0f9f48 chore(production): remove appsettings.Staging.json — config comes from K8s Secret
Build and Deploy to Production / build-and-deploy (push) Successful in 12m5s
DO NOT cherry-pick this commit to kub-stage branch
2026-02-23 22:10:53 +03:30
masoodafar-web f4e24f7449 feat: externalize appsettings to K8s Secret — config persists independently of image rebuilds
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
- Add cms-config.yaml (K8s Secret) for staging and production
- Mount appsettings.{Environment}.json from Secret into /app/
- Update CI/CD pipelines to apply Secret before Deployment
- Remove redundant env vars (Kestrel, FileStorage) — now in config file
2026-02-23 21:58:56 +03:30
masoodafar-web 540a2ce4fc fix: production uses namespace default + remove foursat namespace references
Build and Deploy to Production / build-and-deploy (push) Successful in 8m44s
2026-02-23 21:36:16 +03:30
masoodafar-web e57f01965a fix: staging ASPNETCORE_ENVIRONMENT=Staging, remove secretKeyRef (config in appsettings)
Build and Deploy to Production / build-and-deploy (push) Failing after 8m36s
2026-02-23 21:17:34 +03:30
masoodafar-web f97c4b0812 fix: use local registry 194.5.195.53:30080 instead of git.se.kbs1.ir for image pull
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-02-23 20:58:28 +03:30
masoodafar-web fd32e15192 fix: production ingress — add cms.kbs2.ir, use ingressClassName instead of annotation
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-02-23 20:42:28 +03:30
masoodafar-web 4456e504d4 feat: add PersistentVolume for CMS uploads + apply manifests in CI/CD
Build and Deploy to Production / build-and-deploy (push) Failing after 8m12s
- Add k8s/staging/ and k8s/production/ deployment manifests
- PVC 20Gi for /app/Uploads (file uploads persist across pod restarts)
- FileStorage__UploadPath env var set to /app/Uploads
- Replicas reduced to 1 (ReadWriteOnce PVC)
- Pipelines now: create namespace → apply manifests → restart/update
2026-02-23 20:12:24 +03:30
masoodafar-web cec1e74f6c chore: prevent appsettings.Production.json from merging — keep ours strategy
Build and Deploy to Production / build-and-deploy (push) Successful in 10m59s
2026-02-22 23:59:36 +03:30
masoodafar-web dd9ac48474 fix: update CMS and FrontOffice base URLs to reflect new domain
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-02-22 23:53:56 +03:30
masoodafar-web eb1b249869 Merge kub-stage into production
Build and Deploy to Production / build-and-deploy (push) Successful in 11m49s
- Resolved appsettings.Production.json conflict (new MerchantId, SeedWorkers)
- Removed duplicate u21 migration (kept 155925 from production)
- All features: Magic Wallet, Discount Wallet, StockMovement fix, FullInformation expansion, Wallet user_name, gateway activation
2026-02-22 22:21:36 +03:30
masoodafar-web e8e964f9a0 Add migration for UserWallets and ClubMembershipCycles schema updates
- Added columns to UserWallets: MagicActivatedAt, MagicCompletedAt, MagicTotalCredited, MagicTotalDeposited, and WalletMode.
- Created new table ClubMembershipCycles with relevant fields and constraints.
- Established foreign key relationships with Users and ClubMemberships.
- Added indexes for optimized queries on ClubMembershipCycles.
2026-02-22 19:33:56 +03:30
masoodafar-web 9050c00297 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 8m56s
2026-02-21 20:37:55 +03:30
masoodafar-web 6048824b33 docs: move README to totalDoc, replace with placeholder 2026-02-18 21:44:39 +03:30
masoodafar-web 62a247f097 merge: kub-stage → production (inventory auto-init + initializer worker)
Build and Deploy to Production / build-and-deploy (push) Successful in 9m2s
2026-02-18 01:01:15 +03:30
masoodafar-web af12f996f1 merge: kub-stage into production — fix prod workflow + add CmsBaseUrl/FrontOfficeBaseUrl
Build and Deploy to Production / build-and-deploy (push) Successful in 8m9s
2026-02-17 01:59:28 +03:30
masoodafar-web 3b0d9eadd3 Fix production workflow: use SERVER_PASSWORD, add docker login, use set image for deploy
Build and Deploy to Production / build-and-deploy (push) Successful in 8m17s
2026-02-17 01:23:53 +03:30
masoodafar-web db07838ecb Remove staging workflow files from production branch to prevent cross-deployment 2026-02-17 01:15:50 +03:30
masoodafar-web 167991890c Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 2m43s
Features merged:
- ZarinPal payment gateway (sandbox mode)
- Discount store improvements (100% discount auto-apply)
- VAT breakdown on checkout
- PaymentStatus in order responses (proto v0.0.179)
- ExpirePendingOrdersService (background job for stale orders)
- DeliveryStatus proper mapping + cancel on failed payment
- Inventory management system
- Various bug fixes

Production config preserved:
- DB: 45.149.79.127 / KBS
- CmsBaseUrl: https://cms.kbs1.ir
- FrontOfficeBaseUrl: https://foursat.kbs1.ir
- Payment gateway: sandbox (will show 'درگاه فعال نمیباشد')
- Production workflow unchanged
2026-02-17 00:53:28 +03:30
masoodafar-web 1a425f0d93 chore: Update production workflow to use Nexus registry (no proxy needed)
Build and Deploy to Production / build-and-deploy (push) Failing after 0s
- Changed Dockerfile base images to use 194.5.195.53:32082 (Nexus)
- Updated workflow to use docker-sshpass image from Nexus
- Removed HTTP_PROXY/HTTPS_PROXY dependencies
- Changed deploy method to SSH instead of kubeconfig
- Registry: 194.5.195.53:30080 (Gitea internal)
- K8S Server: 45.149.79.127 (Production)
2026-01-29 22:04:31 +03:30
masoodafar-web 6f8aefc2ce Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 1m40s
2025-12-29 00:36:51 +03:30
masoodafar-web 2bd5c7db78 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 1m50s
2025-12-27 07:37:30 +03:30
masoodafar-web 0728ec2b76 feat: add background job configuration for weekly commission calculation
Build and Deploy to Production / build-and-deploy (push) Successful in 3m18s
2025-12-27 01:51:32 +03:30
masoodafar-web 1425fb187b Merge branch 'kub-stage' into production
# Conflicts:
#	src/CMSMicroservice.WebApi/Program.cs
2025-12-27 01:49:11 +03:30
masoodafar-web 81d2b39ee1 fix: update Seq ServerUrl and change Hangfire job timezone to local - Adjusted Seq server URL for production and modified recurring job options to use local timezone 2025-12-27 01:14:31 +03:30
masoodafar-web fce194a195 feat: update connection strings and add production configuration - Modified database connection details and created appsettings.Production.json for production environment
Build and Deploy to Production / build-and-deploy (push) Successful in 4m2s
2025-12-27 00:16:36 +03:30
107 changed files with 340 additions and 12896 deletions
-46
View File
@@ -1,46 +0,0 @@
name: Push nuget and docker image Actions Workflow
on:
push:
branches:
- stage_new
jobs:
Deploy:
runs-on: windows
steps:
- name: Checkout
uses: https://git.afrino.co/actions/checkout@v3
- name: Setup dotnet
uses: https://git.afrino.co/actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Remove Package Source
run: dotnet nuget remove source FourSat
continue-on-error: true
- name: Add Package Source
run: dotnet nuget add source --name FourSat --username systemuser --password sZSA7PTiv3pUSQZ https://git.afrino.co/api/packages/FourSat/nuget/index.json --store-password-in-clear-text
- name: Install dependencies
run: dotnet restore ".\src\CMSMicroservice.WebApi\CMSMicroservice.WebApi.csproj"
- name: Build
run: dotnet build ".\src\CMSMicroservice.WebApi\CMSMicroservice.WebApi.csproj" --configuration Release --no-restore
- name: Test
run: dotnet test ".\src\CMSMicroservice.WebApi\CMSMicroservice.WebApi.csproj" --no-restore --verbosity normal
- name: Recycle Apppool
run: |
& "C:\Windows\System32\inetsrv\appcmd.exe" recycle apppool /apppool.name:cms.kbs1.ir
shell: powershell
- name: Stop Website
run: |
& "C:\Windows\System32\inetsrv\appcmd.exe" stop site /site.name:cms.kbs1.ir
shell: powershell
- name: Publish
run: dotnet publish ".\src\CMSMicroservice.WebApi\CMSMicroservice.WebApi.csproj" -c Release -o publish
- name: Copy Publish To IIS Directory
run: Get-ChildItem -Path "publish\*" | Copy-Item -Destination "E:\kbs1.ir\cms.kbs1.ir\" -Recurse -Force
- name: Start Website
run: |
& "C:\Windows\System32\inetsrv\appcmd.exe" start site /site.name:cms.kbs1.ir
shell: powershell
+10 -10
View File
@@ -6,15 +6,15 @@ on:
- kub-stage
env:
REGISTRY: 185.252.31.44:30080
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/cms
K8S_SERVER: 185.252.31.44
K8S_SERVER: 194.5.195.53
jobs:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: 185.252.31.44:32082/docker-sshpass:latest
image: 194.5.195.53:32082/docker-sshpass:latest
options: --privileged
steps:
- name: Start Docker daemon
@@ -22,7 +22,7 @@ jobs:
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["185.252.31.44:30080", "185.252.31.44:32500", "185.252.31.44:32082"]
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
}
DAEMON
echo "🚀 Starting Docker daemon..."
@@ -52,14 +52,14 @@ jobs:
- name: Login to Docker registries
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 185.252.31.44:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
- name: Publish Protobuf packages
run: |
echo "📦 Publishing Protobuf packages..."
docker run --rm --network host -e CI=true -v $(pwd):/src -w /src \
185.252.31.44:32082/dotnet/sdk:9.0 sh -c '
194.5.195.53:32082/dotnet/sdk:9.0 sh -c '
for proj in $(find . -name "*Protobuf*.csproj" -type f); do
echo "📦 $proj"
dotnet restore "$proj" --configfile src/NuGet.config
@@ -93,11 +93,11 @@ jobs:
# Apply config (Secret) first, then deployment
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
kubectl apply -n kbs-stage -f /tmp/cms-config.yaml &&
kubectl apply -n kbs-stage -f /tmp/cms-deployment.yaml &&
kubectl apply -f /tmp/cms-config.yaml &&
kubectl apply -f /tmp/cms-deployment.yaml &&
crictl rmi ${REGISTRY}/${IMAGE_NAME}:latest 2>/dev/null || true &&
kubectl -n kbs-stage rollout restart deployment/cms &&
kubectl -n kbs-stage rollout status deployment/cms --timeout=180s &&
kubectl rollout restart deployment/cms &&
kubectl rollout status deployment/cms --timeout=180s &&
rm -f /tmp/cms-config.yaml /tmp/cms-deployment.yaml
"
echo "✅ Deployed!"
+11 -9
View File
@@ -6,7 +6,7 @@ on:
- production
env:
REGISTRY: 185.252.31.44:30080
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/cms
K8S_SERVER: 45.149.79.127
@@ -14,7 +14,7 @@ jobs:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: 185.252.31.44:32082/docker-sshpass:latest
image: 194.5.195.53:32082/docker-sshpass:latest
options: --privileged
steps:
- name: Start Docker daemon
@@ -22,7 +22,7 @@ jobs:
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["185.252.31.44:30080", "185.252.31.44:32500", "185.252.31.44:32082"]
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
}
DAEMON
echo "🚀 Starting Docker daemon..."
@@ -40,7 +40,7 @@ jobs:
done
if ! docker info >/dev/null 2>&1; then
echo "❌ Docker daemon failed to start after 3 minutes"
echo "❌ Docker daemon failed to start"
exit 1
fi
@@ -50,14 +50,14 @@ jobs:
- name: Login to Docker registries
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 185.252.31.44:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
- name: Publish Protobuf packages
run: |
echo "📦 Publishing Protobuf packages..."
docker run --rm --network host -v $(pwd):/src -w /src \
185.252.31.44:32082/dotnet/sdk:9.0 sh -c '
194.5.195.53:32082/dotnet/sdk:9.0 sh -c '
for proj in $(find . -name "*Protobuf*.csproj" -type f); do
echo "📦 $proj"
dotnet restore "$proj" --configfile src/NuGet.config
@@ -65,7 +65,7 @@ jobs:
dotnet pack "$proj" -c Release --no-build -o "$(dirname $proj)/nupkg"
for nupkg in $(dirname $proj)/nupkg/*.nupkg; do
[ -f "$nupkg" ] && dotnet nuget push "$nupkg" \
--source "http://185.252.31.44:32081/repository/foursat-nuget-hosted/index.json" \
--source "http://194.5.195.53:32081/repository/foursat-nuget-hosted/index.json" \
--api-key "admin:87zH26nbqT" \
--skip-duplicate --allow-insecure-connections || true
done
@@ -75,8 +75,10 @@ jobs:
- name: Build Docker Image
run: |
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod .
DOCKER_BUILDKIT=0 docker build --network host \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
.
- name: Push to Registry
run: |
+3 -3
View File
@@ -1,4 +1,4 @@
FROM 185.252.31.44:32082/dotnet/sdk:9.0 AS build
FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build
WORKDIR /src
# Copy NuGet config first
@@ -7,11 +7,11 @@ COPY src/NuGet.config ./
# Copy solution and project files
COPY src/ ./
# Restore with Nexus config and publish
# Restore with Nexus config and publish
RUN dotnet restore "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj" --configfile NuGet.config
RUN dotnet publish "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj" -c Release -o /app/publish --no-restore
FROM 185.252.31.44:32082/dotnet/aspnet:9.0 AS runtime
FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:8080
+2 -2
View File
@@ -1,4 +1,4 @@
# CMS Microservice
Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
-18
View File
@@ -24,24 +24,6 @@ stringData:
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
"UseSandbox": false
},
"TwentyCard": {
"BaseUrl": "https://gateway.20card.ir/ipg",
"TerminalId": "165674",
"TerminalUsername": "ea5y1biehi",
"TerminalPassword": "SaLamAT@1404!",
"CreditPortalUrl": "https://userpanel.20card.ir/",
"DefaultEmail": "info@kbs2.ir",
"Packages": {
"4": {
"GatewayAmountToman": 11200000,
"WalletCreditToman": 12656000
},
"1": {
"GatewayAmountToman": 11200000,
"WalletCreditToman": 12656000
}
}
},
"CmsBaseUrl": "https://cms.kbs2.ir",
"FrontOfficeBaseUrl": "https://kbs2.ir",
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
+1 -1
View File
@@ -35,7 +35,7 @@ spec:
spec:
containers:
- name: cms
image: 185.252.31.44:30080/admin/cms:prod
image: 194.5.195.53:30080/admin/cms:prod
imagePullPolicy: Always
ports:
- containerPort: 8080
+2 -21
View File
@@ -11,7 +11,7 @@ apiVersion: v1
kind: Secret
metadata:
name: cms-appsettings
namespace: kbs-stage
namespace: default
labels:
app: cms
environment: staging
@@ -24,25 +24,6 @@ stringData:
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
"UseSandbox": true
},
"TwentyCard": {
"BaseUrl": "https://gateway.20card.ir/ipg",
"TerminalId": "165674",
"TerminalUsername": "ea5y1biehi",
"TerminalPassword": "SaLamAT@1404!",
"CreditPortalUrl": "https://userpanel.20card.ir/",
"CallbackBaseUrl": "https://kbs2.ir",
"DefaultEmail": "info@kbs2.ir",
"Packages": {
"4": {
"GatewayAmountToman": 11200000,
"WalletCreditToman": 12656000
},
"1": {
"GatewayAmountToman": 11200000,
"WalletCreditToman": 12656000
}
}
},
"CmsBaseUrl": "https://cms.se.kbs1.ir",
"FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir",
"FMS": {
@@ -53,7 +34,7 @@ stringData:
"JwtAudience": "https://localhost",
"JwtExpiryInDays": 5,
"ConnectionStrings": {
"DefaultConnection": "Data Source=mssql-svc.platform.svc.cluster.local,1433; Initial Catalog=Foursat;User ID=sa;Password=87zH26nbqT;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
"DefaultConnection": "Data Source=194.5.195.53,31433; Initial Catalog=Foursat;User ID=sa;Password=87zH26nbqT;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
"providerName": "System.Data.SqlClient"
},
"Otp": {
+6 -11
View File
@@ -4,14 +4,12 @@ apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cms-uploads-pvc
namespace: kbs-stage
namespace: default
labels:
app: cms
spec:
accessModes:
- ReadWriteOnce
storageClassName: migrated-local
volumeName: pv-cms-uploads-migrated
resources:
requests:
storage: 20Gi
@@ -22,7 +20,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: cms
namespace: kbs-stage
namespace: default
labels:
app: cms
spec:
@@ -35,12 +33,10 @@ spec:
labels:
app: cms
spec:
imagePullSecrets:
- name: gitea-registry-secret
containers:
- name: cms
image: 185.252.31.44:30080/admin/cms:latest
imagePullPolicy: IfNotPresent
image: 194.5.195.53:30080/admin/cms:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
name: http
@@ -93,7 +89,7 @@ apiVersion: v1
kind: Service
metadata:
name: cms-svc
namespace: kbs-stage
namespace: default
spec:
selector:
app: cms
@@ -111,7 +107,7 @@ apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cms-ingress
namespace: kbs-stage
namespace: default
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
@@ -119,7 +115,6 @@ metadata:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
ingressClassName: nginx
tls:
- hosts:
- cms.se.kbs1.ir
-340
View File
@@ -1,340 +0,0 @@
/*
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
@@ -15,7 +15,6 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.4.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.10" />
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
@@ -163,10 +163,10 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
request.UserId
);
// در فعال‌سازی اجباری ادمین، اگر روش خرید تنظیم نشده، دستی در نظر بگیر
// در فعال‌سازی اجباری، اگر PackagePurchaseMethod تنظیم نشده، مقدار DirectPurchase بگذار
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
{
user.PackagePurchaseMethod = PackagePurchaseMethod.Manual;
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
_context.Users.Update(user);
await _context.SaveChangesAsync(cancellationToken);
}
@@ -1,5 +1,3 @@
using CMSMicroservice.Application.Common;
namespace CMSMicroservice.Application.CommissionCQ.Commands.RequestWithdrawal;
public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawalCommand, Unit>
@@ -39,10 +37,7 @@ public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawal
if (request.Method == WithdrawalMethod.Cash)
{
var normalizedIban = IbanNormalizer.TryNormalize(request.IbanNumber);
if (normalizedIban is null)
throw new InvalidOperationException("فرمت شماره شبا معتبر نیست. باید IR و ۲۴ رقم باشد.");
payout.IbanNumber = normalizedIban;
payout.IbanNumber = request.IbanNumber;
}
_context.UserCommissionPayouts.Update(payout);
@@ -1,34 +0,0 @@
using System.Text.RegularExpressions;
namespace CMSMicroservice.Application.Common;
/// <summary>
/// نرمال‌سازی و اعتبارسنجی شماره شبا ایران (IR + 24 رقم).
/// </summary>
public static class IbanNormalizer
{
private static readonly Regex IranianIbanRegex = new(@"^IR\d{24}$", RegexOptions.Compiled);
/// <summary>
/// فاصله و خط تیره را حذف می‌کند، به حروف بزرگ تبدیل می‌کند و یک پیشوند IR می‌گذارد.
/// در صورت نامعتبر بودن، null برمی‌گرداند.
/// </summary>
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;
}
@@ -21,7 +21,6 @@ public interface IApplicationDbContext
DbSet<Product> Products { get; }
DbSet<ProductImage> ProductImages { get; }
DbSet<User> Users { get; }
DbSet<UserInvitation> UserInvitations { get; }
DbSet<OtpToken> OtpTokens { get; }
DbSet<Contract> Contracts { get; }
DbSet<UserContract> UserContracts { get; }
@@ -1,9 +0,0 @@
namespace CMSMicroservice.Application.Common.Interfaces;
/// <summary>
/// Resolve payment gateway by provider key (zarinpal, 20card, …).
/// </summary>
public interface IPaymentGatewayResolver
{
IPaymentGatewayService Get(string? provider);
}
@@ -98,26 +98,6 @@ public class PaymentRequest
/// URL بازگشت بعد از پرداخت
/// </summary>
public string CallbackUrl { get; set; } = string.Empty;
/// <summary>
/// نام پرداخت‌کننده (برای درگاه‌هایی مثل ۲۰کارت)
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// شماره فاکتور / سفارش
/// </summary>
public string Invoice { get; set; } = string.Empty;
/// <summary>
/// ایمیل (اختیاری — برای ۲۰کارت)
/// </summary>
public string? Email { get; set; }
/// <summary>
/// کد ملی (اختیاری — برای ۲۰کارت)
/// </summary>
public string? NationalId { get; set; }
}
/// <summary>
@@ -1,29 +0,0 @@
namespace CMSMicroservice.Application.Common;
/// <summary>
/// نام نمایشی کاربر برای گزارش‌ها؛ اگر نام خالی باشد موبایل یا شناسه.
/// </summary>
public static class UserDisplayName
{
public static string From(string? firstName, string? lastName, string? mobile, long userId)
{
var name = string.Join(" ",
new[] { firstName, lastName }.Where(s => !string.IsNullOrWhiteSpace(s)));
if (!string.IsNullOrWhiteSpace(name))
return name;
if (!string.IsNullOrWhiteSpace(mobile))
return mobile.Trim();
return userId > 0 ? $"کاربر {userId}" : "—";
}
public static string From(Domain.Entities.User? user)
{
if (user is null)
return string.Empty;
return From(user.FirstName, user.LastName, user.Mobile, user.Id);
}
}
@@ -35,11 +35,6 @@ public class UpdateOrderStatusCommandHandler : IRequestHandler<UpdateOrderStatus
order.TrackingCode = request.TrackingCode;
}
if (!string.IsNullOrEmpty(request.AdminNotes))
{
order.DeliveryDescription = request.AdminNotes;
}
await _context.SaveChangesAsync(cancellationToken);
return new UpdateOrderStatusResponseDto
@@ -1,4 +1,3 @@
using CMSMicroservice.Application.Common;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using MediatR;
@@ -81,58 +80,35 @@ public class GetAllDiscountOrdersQueryHandler : IRequestHandler<GetAllDiscountOr
// Apply pagination
var pagination = request.PaginationQuery ?? new PaginationState { PageNumber = 1, PageSize = 20 };
var rows = await query
var orders = await query
.OrderByDescending(o => o.Created)
.Skip((pagination.PageNumber - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.Select(o => new
.Select(o => new AdminOrderDto
{
o.Id,
o.UserId,
FirstName = o.User.FirstName,
LastName = o.User.LastName,
Mobile = o.User.Mobile,
o.TotalAmount,
o.DiscountBalanceUsed,
o.GatewayAmountPaid,
o.VatAmount,
o.PaymentStatus,
o.PaymentDate,
o.DeliveryStatus,
Id = o.Id,
UserId = o.UserId,
UserFullName = (o.User.FirstName ?? "") + " " + (o.User.LastName ?? ""),
UserMobile = o.User.Mobile,
TotalAmount = o.TotalAmount,
DiscountBalanceUsed = o.DiscountBalanceUsed,
GatewayAmountPaid = o.GatewayAmountPaid,
VatAmount = o.VatAmount,
PaymentStatus = o.PaymentStatus,
PaymentDate = o.PaymentDate,
DeliveryStatus = o.DeliveryStatus,
DeliveryDate = null, // TODO: Add DeliveryDate to DiscountOrder if needed
ShippingAddress = o.UserAddress.Address,
ReceiverName = o.UserAddress.Title,
o.TrackingCode,
ReceiverMobile = o.User.Mobile,
TrackingCode = o.TrackingCode,
AdminNote = o.DeliveryDescription,
o.Created,
o.LastModified,
Created = o.Created,
LastModified = o.LastModified,
ItemsCount = o.OrderDetails.Count
})
.ToListAsync(cancellationToken);
var orders = rows.Select(o => new AdminOrderDto
{
Id = o.Id,
UserId = o.UserId,
UserFullName = UserDisplayName.From(o.FirstName, o.LastName, o.Mobile, o.UserId),
UserMobile = o.Mobile,
TotalAmount = o.TotalAmount,
DiscountBalanceUsed = o.DiscountBalanceUsed,
GatewayAmountPaid = o.GatewayAmountPaid,
VatAmount = o.VatAmount,
PaymentStatus = o.PaymentStatus,
PaymentDate = o.PaymentDate,
DeliveryStatus = o.DeliveryStatus,
DeliveryDate = null,
ShippingAddress = o.ShippingAddress,
ReceiverName = o.ReceiverName,
ReceiverMobile = o.Mobile,
TrackingCode = o.TrackingCode,
AdminNote = o.AdminNote,
Created = o.Created,
LastModified = o.LastModified,
ItemsCount = o.ItemsCount
}).ToList();
return new GetAllDiscountOrdersResponseDto
{
MetaData = new MetaData
@@ -32,8 +32,6 @@ public class UserAddressDto
public string Title { get; set; }
public string Address { get; set; }
public string PostalCode { get; set; }
/// <summary>شماره تماس سفارش‌دهنده (موبایل کاربر) برای برچسب پست</summary>
public string? Phone { get; set; }
}
public class OrderItemDto
@@ -25,7 +25,6 @@ public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, Order
var order = await query
.Include(o => o.UserAddress)
.Include(o => o.User)
.Include(o => o.OrderDetails)
.ThenInclude(od => od.Product)
.FirstOrDefaultAsync(cancellationToken);
@@ -51,8 +50,7 @@ public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, Order
{
Title = order.UserAddress.Title,
Address = order.UserAddress.Address,
PostalCode = order.UserAddress.PostalCode,
Phone = order.User?.Mobile
PostalCode = order.UserAddress.PostalCode
},
Items = order.OrderDetails.Select(od => new OrderItemDto
{
@@ -150,18 +150,8 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
await _context.UserWalletHistories.AddAsync(walletLog, cancellationToken);
// 9. تنظیم روش خرید پکیج + ثبت ledger خرید دستی
user.PackagePurchaseMethod = PackagePurchaseMethod.Manual;
_context.UserPackagePurchases.Add(new UserPackagePurchase
{
UserId = request.UserId,
PackageId = package.Id,
PurchaseMethod = PackagePurchaseMethod.Manual,
PurchasedAt = DateTime.Now,
Amount = balanceAmount,
TransactionId = transaction.Id
});
// 9. تنظیم روش خرید پکیج
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
// 10. ذخیره همه تغییرات
await _context.SaveChangesAsync(cancellationToken);
@@ -118,7 +118,7 @@ public class ProcessManualMembershipPaymentCommandHandler : IRequestHandler<Proc
};
await _context.UserWalletHistories.AddAsync(balanceLog, cancellationToken);
user.PackagePurchaseMethod = PackagePurchaseMethod.Manual;
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
// 10. به‌روزرسانی ManualPayment با TransactionId
manualPayment.TransactionId = transaction.Id;
await _context.SaveChangesAsync(cancellationToken);
@@ -1,5 +1,4 @@
using CMSMicroservice.Domain.Enums;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStatistics;
@@ -35,28 +34,17 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
}
var allDescendants = GetAllDescendants(userId, allUsers);
// Statistics for the user's network (all descendants)
var totalMembers = allDescendants.Count;
var activeMembers = allDescendants.Count(x => !x.IsDeleted);
// Get direct left and right children
var leftChild = allUsers.FirstOrDefault(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Left);
var rightChild = allUsers.FirstOrDefault(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Right);
var leftLegUserIds = leftChild != null ? GetSubtreeUserIds(leftChild.Id, allUsers) : new HashSet<long>();
var rightLegUserIds = rightChild != null ? GetSubtreeUserIds(rightChild.Id, allUsers) : new HashSet<long>();
var allLegUserIds = leftLegUserIds.Union(rightLegUserIds).ToList();
// سازمان چپ/راست = فقط اعضای فعال باشگاه در هر پا
var activeClubUserIds = allLegUserIds.Count == 0
? new HashSet<long>()
: (await _context.ClubMemberships
.AsNoTracking()
.Where(cm => cm.IsActive && !cm.IsDeleted && allLegUserIds.Contains(cm.UserId))
.Select(cm => cm.UserId)
.ToListAsync(cancellationToken))
.ToHashSet();
var leftLegCount = leftLegUserIds.Count(id => activeClubUserIds.Contains(id));
var rightLegCount = rightLegUserIds.Count(id => activeClubUserIds.Contains(id));
var totalMembers = leftLegCount + rightLegCount;
var activeMembers = totalMembers;
// Count all descendants in left and right subtrees
var leftLegCount = leftChild != null ? GetAllDescendants(leftChild.Id, allUsers).Count + 1 : 0; // +1 for leftChild itself
var rightLegCount = rightChild != null ? GetAllDescendants(rightChild.Id, allUsers).Count + 1 : 0; // +1 for rightChild itself
double leftPercentage = totalMembers > 0 ? (leftLegCount / (double)totalMembers) * 100 : 0;
double rightPercentage = totalMembers > 0 ? (rightLegCount / (double)totalMembers) * 100 : 0;
@@ -136,9 +124,6 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
RightCount = x.RightCount
}).ToList();
var (goldLeft, goldRight, silverLeft, silverRight) =
await LoadPackageHolderCountsByLegAsync(leftLegUserIds, rightLegUserIds, cancellationToken);
return new GetNetworkStatisticsResponseDto
{
TotalMembers = totalMembers,
@@ -151,93 +136,10 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
MaxDepth = maxDepth,
LevelDistribution = levelDistribution,
MonthlyGrowth = monthlyGrowth,
TopUsers = topUserModels,
GoldLeftLegTotal = goldLeft,
GoldRightLegTotal = goldRight,
SilverLeftLegTotal = silverLeft,
SilverRightLegTotal = silverRight
TopUsers = topUserModels
};
}
/// <summary>
/// تعداد اعضای فعال باشگاه با پکیج جاری (IsCurrentCycle) در پای چپ/راست.
/// </summary>
private async Task<(int GoldLeft, int GoldRight, int SilverLeft, int SilverRight)> LoadPackageHolderCountsByLegAsync(
HashSet<long> leftLegUserIds,
HashSet<long> rightLegUserIds,
CancellationToken cancellationToken)
{
if (leftLegUserIds.Count == 0 && rightLegUserIds.Count == 0)
return (0, 0, 0, 0);
var (goldPackageId, silverPackageId) = await ResolveGoldSilverPackageIdsAsync(cancellationToken);
if (!goldPackageId.HasValue && !silverPackageId.HasValue)
return (0, 0, 0, 0);
var allLegUserIds = leftLegUserIds.Union(rightLegUserIds).ToList();
var userPackages = await (
from cm in _context.ClubMemberships.AsNoTracking()
join cc in _context.ClubMembershipCycles.AsNoTracking() on cm.UserId equals cc.UserId
where cm.IsActive
&& !cm.IsDeleted
&& cc.IsCurrentCycle
&& allLegUserIds.Contains(cm.UserId)
select new { cm.UserId, cc.PackageId }
).ToListAsync(cancellationToken);
var packageByUser = userPackages
.GroupBy(x => x.UserId)
.ToDictionary(g => g.Key, g => g.First().PackageId);
int goldLeft = 0, goldRight = 0, silverLeft = 0, silverRight = 0;
if (goldPackageId.HasValue)
{
goldLeft = leftLegUserIds.Count(id =>
packageByUser.TryGetValue(id, out var packageId) && packageId == goldPackageId.Value);
goldRight = rightLegUserIds.Count(id =>
packageByUser.TryGetValue(id, out var packageId) && packageId == goldPackageId.Value);
}
if (silverPackageId.HasValue)
{
silverLeft = leftLegUserIds.Count(id =>
packageByUser.TryGetValue(id, out var packageId) && packageId == silverPackageId.Value);
silverRight = rightLegUserIds.Count(id =>
packageByUser.TryGetValue(id, out var packageId) && packageId == silverPackageId.Value);
}
return (goldLeft, goldRight, silverLeft, silverRight);
}
private async Task<(long? GoldPackageId, long? SilverPackageId)> ResolveGoldSilverPackageIdsAsync(
CancellationToken cancellationToken)
{
var packages = await _context.Packages
.AsNoTracking()
.Where(p => !p.IsDeleted && p.IsActive)
.Select(p => new { p.Id, p.Title, p.IsBasePackage, p.MaxBalancesPerLeg })
.ToListAsync(cancellationToken);
var goldPackageId = packages.FirstOrDefault(p => p.IsBasePackage)?.Id
?? packages.FirstOrDefault(p => p.Title.Contains("طلایی") || p.Title.Contains("طلايی"))?.Id;
var silverPackageId = packages.FirstOrDefault(p =>
!p.IsBasePackage && (p.Title.Contains("نقره") || p.MaxBalancesPerLeg <= 30))?.Id
?? packages.FirstOrDefault(p => !p.IsBasePackage)?.Id;
return (goldPackageId, silverPackageId);
}
private HashSet<long> GetSubtreeUserIds(long rootUserId, List<User> allUsers)
{
var ids = new HashSet<long> { rootUserId };
foreach (var descendant in GetAllDescendants(rootUserId, allUsers))
ids.Add(descendant.Id);
return ids;
}
/// <summary>
/// Recursively get all descendants of a user
/// </summary>
@@ -2,13 +2,9 @@ namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStat
public class GetNetworkStatisticsResponseDto
{
/// <summary>مجموع اعضای فعال باشگاه در پای چپ و راست (= LeftLegCount + RightLegCount)</summary>
public int TotalMembers { get; set; }
/// <summary>همان TotalMembers — اعضای فعال باشگاه در شبکه کاربر</summary>
public int ActiveMembers { get; set; }
/// <summary>تعداد اعضای فعال باشگاه در زیردرخت پای چپ</summary>
public int LeftLegCount { get; set; }
/// <summary>تعداد اعضای فعال باشگاه در زیردرخت پای راست</summary>
public int RightLegCount { get; set; }
public double LeftPercentage { get; set; }
public double RightPercentage { get; set; }
@@ -17,17 +13,6 @@ public class GetNetworkStatisticsResponseDto
public List<LevelDistributionModel> LevelDistribution { get; set; } = new();
public List<MonthlyGrowthModel> MonthlyGrowth { get; set; } = new();
public List<TopNetworkUserModel> TopUsers { get; set; } = new();
/// <summary>تعداد اعضای فعال باشگاه با پکیج طلایی جاری در پای چپ</summary>
public int GoldLeftLegTotal { get; set; }
/// <summary>تعداد اعضای فعال باشگاه با پکیج طلایی جاری در پای راست</summary>
public int GoldRightLegTotal { get; set; }
/// <summary>تعداد اعضای فعال باشگاه با پکیج نقره‌ای جاری در پای چپ</summary>
public int SilverLeftLegTotal { get; set; }
/// <summary>تعداد اعضای فعال باشگاه با پکیج نقره‌ای جاری در پای راست</summary>
public int SilverRightLegTotal { get; set; }
public string WeakerLeg => LeftLegCount <= RightLegCount ? "Left" : "Right";
}
public class LevelDistributionModel
@@ -1,4 +1,5 @@
using System.Data;
using System.Data.Common;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
@@ -60,18 +60,6 @@ public class NetworkTreeDto
public long? PackageId { get; set; }
/// <summary>تعداد اعضای فعال با پکیج طلایی جاری در پای چپ (stats bar)</summary>
public int GoldLeftLegTotal { get; set; }
/// <summary>تعداد اعضای فعال با پکیج طلایی جاری در پای راست (stats bar)</summary>
public int GoldRightLegTotal { get; set; }
/// <summary>تعداد اعضای فعال با پکیج نقره‌ای جاری در پای چپ (stats bar)</summary>
public int SilverLeftLegTotal { get; set; }
/// <summary>تعداد اعضای فعال با پکیج نقره‌ای جاری در پای راست (stats bar)</summary>
public int SilverRightLegTotal { get; set; }
public NetworkTreeDto? LeftChild { get; set; }
public NetworkTreeDto? RightChild { get; set; }
}
@@ -8,7 +8,6 @@ using CMSMicroservice.Domain.Entities.Payment;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
@@ -17,23 +16,20 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
: IRequestHandler<VerifyUserPackagePurchasePaymentCommand, VerifyUserPackagePurchasePaymentResult>
{
private readonly IApplicationDbContext _context;
private readonly IPaymentGatewayResolver _paymentGatewayResolver;
private readonly IConfiguration _configuration;
private readonly IPaymentGatewayService _paymentGateway;
private readonly ISender _sender;
private readonly IUserPaymentLock _paymentLock;
private readonly ILogger<VerifyUserPackagePurchasePaymentCommandHandler> _logger;
public VerifyUserPackagePurchasePaymentCommandHandler(
IApplicationDbContext context,
IPaymentGatewayResolver paymentGatewayResolver,
IConfiguration configuration,
IPaymentGatewayService paymentGateway,
ISender sender,
IUserPaymentLock paymentLock,
ILogger<VerifyUserPackagePurchasePaymentCommandHandler> logger)
{
_context = context;
_paymentGatewayResolver = paymentGatewayResolver;
_configuration = configuration;
_paymentGateway = paymentGateway;
_sender = sender;
_paymentLock = paymentLock;
_logger = logger;
@@ -109,8 +105,7 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
}
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
var gateway = _paymentGatewayResolver.Get(paymentTx?.GatewayProvider);
var verifyResult = await gateway.VerifyPaymentAsync(
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority, request.Status, (decimal)amountInToman, ct);
if (!verifyResult.IsSuccess)
@@ -198,30 +193,17 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
await _context.SaveChangesAsync(ct);
}
var packagePrice = purchase.Package?.Price
var discountMultiplier = purchase.Package?.DiscountMultiplier
?? throw new InvalidOperationException("پکیج سفارش یافت نشد");
var discountMultiplier = purchase.Package.DiscountMultiplier;
var discountAmount = (long)(packagePrice * (double)discountMultiplier);
var isTwentyCard = string.Equals(
paymentTx?.GatewayProvider, "20card", StringComparison.OrdinalIgnoreCase);
long balanceCredit = purchase.Amount;
if (isTwentyCard)
{
balanceCredit = _configuration.GetValue<long?>(
$"TwentyCard:Packages:{purchase.PackageId}:WalletCreditToman") ?? 0;
if (balanceCredit <= 0)
throw new InvalidOperationException("WalletCredit ۲۰کارت برای این پکیج پیکربندی نشده است");
}
wallet.Balance += balanceCredit;
var discountAmount = (long)(purchase.Amount * (double)discountMultiplier);
wallet.Balance += purchase.Amount;
wallet.DiscountBalance += discountAmount;
_context.UserWalletHistories.Add(new UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = balanceCredit,
ChangeValue = purchase.Amount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
@@ -43,5 +43,4 @@ public class GetAllPaymentTransactionByFilterResponseModel
public long? TransactionId { get; set; }
public string? Description { get; set; }
public DateTimeOffset Created { get; set; }
public string? UserFullName { get; set; }
}
@@ -19,43 +19,30 @@ 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 = models
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)
};
}
}
@@ -1,5 +1,3 @@
using CMSMicroservice.Application.Common;
namespace CMSMicroservice.Application.TransactionsCQ.Queries.GetAllTransactionsByFilter;
public class GetAllTransactionsByFilterQueryHandler : IRequestHandler<GetAllTransactionsByFilterQuery, GetAllTransactionsByFilterResponseDto>
@@ -18,106 +16,23 @@ public class GetAllTransactionsByFilterQueryHandler : IRequestHandler<GetAllTran
.ApplyTransactionFilter(request.Filter)
.ApplyOrder(sortBy: request.SortBy ?? "-Created");
var 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);
await PopulateUserInfoAsync(models, cancellationToken);
return new GetAllTransactionsByFilterResponseDto
{
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
Models = models
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)
};
}
private async Task PopulateUserInfoAsync(
List<GetAllTransactionsByFilterResponseModel> models,
CancellationToken cancellationToken)
{
if (models.Count == 0)
return;
var txIds = models.Select(m => m.Id).ToList();
var userByTx = new Dictionary<long, long>();
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 = "—";
}
}
}
}
@@ -25,7 +25,4 @@ 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; }
}
@@ -9,7 +9,5 @@ public record VerifyOtpTokenCommand : IRequest<VerifyOtpTokenResponseDto>
public string Code { get; init; }
//کد معرف والد
public string? ParentReferralCode { get; init; }
//شماره موبایل دعوت‌کننده (اختیاری)
public string? InviterMobile { get; init; }
}
@@ -118,34 +118,6 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
else
return new VerifyOtpTokenResponseDto { Success = false, Message = "ظرفیت معرف تکمیل است!!" };
// ── دعوت‌کننده (اختیاری؛ اعتبارسنجی سخت قبل از ایجاد کاربر) ──
User? inviter = null;
if (!string.IsNullOrWhiteSpace(request.InviterMobile))
{
var inviterMobile = request.InviterMobile.NormalizeIranMobile();
if (inviterMobile == mobile)
{
return new VerifyOtpTokenResponseDto
{
Success = false,
Message = "شماره موبایل دعوت‌کننده نمی‌تواند با شماره خودتان یکسان باشد."
};
}
inviter = await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Mobile == inviterMobile, cancellationToken);
if (inviter == null)
{
return new VerifyOtpTokenResponseDto
{
Success = false,
Message = "دعوت‌کننده با این شماره موبایل یافت نشد."
};
}
}
// ایجاد کاربر جدید
user = new User
{
@@ -162,15 +134,6 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
user.AddDomainEvent(new CreateNewUserEvent(user));
await _context.SaveChangesAsync(cancellationToken);
if (inviter != null)
{
await _context.UserInvitations.AddAsync(new UserInvitation
{
InviterUserId = inviter.Id,
InviteeUserId = user.Id
}, cancellationToken);
}
// ایجاد نقش کاربری
var userRole = new UserRole { UserId = user.Id, RoleId = 1 };
await _context.UserRoles.AddAsync(userRole, cancellationToken);
@@ -17,7 +17,7 @@ public class GetAllUserByFilterQueryHandler : IRequestHandler<GetAllUserByFilter
if (request.Filter is not null)
{
query = query
.Where(x => 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,39 +34,14 @@ public class GetAllUserByFilterQueryHandler : IRequestHandler<GetAllUserByFilter
.Where(x => 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)
.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)
};
.ProjectToType<GetAllUserByFilterResponseModel>().ToListAsync(cancellationToken)
};
}
}
@@ -36,8 +36,4 @@ 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;
}
@@ -1,46 +0,0 @@
using CMSMicroservice.Application.Common.Models;
using MediatR;
namespace CMSMicroservice.Application.UserCQ.Queries.GetInvitationsByFilter;
public class GetInvitationsByFilterQuery : IRequest<GetInvitationsByFilterResult>
{
public PaginationState? PaginationState { get; set; }
public string? SortBy { get; set; }
public GetInvitationsByFilterFilterDto? Filter { get; set; }
}
public class GetInvitationsByFilterFilterDto
{
public string? InviterMobile { get; set; }
public string? InviterFirstName { get; set; }
public string? InviterLastName { get; set; }
public string? InviteeMobile { get; set; }
public string? InviteeFirstName { get; set; }
public string? InviteeLastName { get; set; }
public bool? InviteeClubActive { get; set; }
public DateTime? CreatedFrom { get; set; }
public DateTime? CreatedTo { get; set; }
}
public class GetInvitationsByFilterResult
{
public MetaData MetaData { get; set; } = new();
public List<InvitationReportDto> Models { get; set; } = new();
}
public class InvitationReportDto
{
public long Id { get; set; }
public long InviterUserId { get; set; }
public string? InviterFirstName { get; set; }
public string? InviterLastName { get; set; }
public string InviterMobile { get; set; } = string.Empty;
public long InviteeUserId { get; set; }
public string? InviteeFirstName { get; set; }
public string? InviteeLastName { get; set; }
public string InviteeMobile { get; set; } = string.Empty;
public DateTime InviteeRegisteredAt { get; set; }
public bool InviteeClubActive { get; set; }
public DateTime InvitedAt { get; set; }
}
@@ -1,96 +0,0 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.UserCQ.Queries.GetInvitationsByFilter;
public class GetInvitationsByFilterQueryHandler
: IRequestHandler<GetInvitationsByFilterQuery, GetInvitationsByFilterResult>
{
private readonly IApplicationDbContext _context;
public GetInvitationsByFilterQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetInvitationsByFilterResult> Handle(
GetInvitationsByFilterQuery request,
CancellationToken cancellationToken)
{
var filter = request.Filter ?? new GetInvitationsByFilterFilterDto();
var query = _context.UserInvitations
.AsNoTracking()
.Select(x => new InvitationReportDto
{
Id = x.Id,
InviterUserId = x.InviterUserId,
InviterFirstName = x.Inviter.FirstName,
InviterLastName = x.Inviter.LastName,
InviterMobile = x.Inviter.Mobile,
InviteeUserId = x.InviteeUserId,
InviteeFirstName = x.Invitee.FirstName,
InviteeLastName = x.Invitee.LastName,
InviteeMobile = x.Invitee.Mobile,
InviteeRegisteredAt = x.Invitee.Created,
InviteeClubActive = x.Invitee.ClubMembership != null && x.Invitee.ClubMembership.IsActive,
InvitedAt = x.Created
});
if (!string.IsNullOrWhiteSpace(filter.InviterMobile))
query = query.Where(x => x.InviterMobile.Contains(filter.InviterMobile));
if (!string.IsNullOrWhiteSpace(filter.InviterFirstName))
query = query.Where(x => x.InviterFirstName != null && x.InviterFirstName.Contains(filter.InviterFirstName));
if (!string.IsNullOrWhiteSpace(filter.InviterLastName))
query = query.Where(x => x.InviterLastName != null && x.InviterLastName.Contains(filter.InviterLastName));
if (!string.IsNullOrWhiteSpace(filter.InviteeMobile))
query = query.Where(x => x.InviteeMobile.Contains(filter.InviteeMobile));
if (!string.IsNullOrWhiteSpace(filter.InviteeFirstName))
query = query.Where(x => x.InviteeFirstName != null && x.InviteeFirstName.Contains(filter.InviteeFirstName));
if (!string.IsNullOrWhiteSpace(filter.InviteeLastName))
query = query.Where(x => x.InviteeLastName != null && x.InviteeLastName.Contains(filter.InviteeLastName));
if (filter.InviteeClubActive.HasValue)
query = query.Where(x => x.InviteeClubActive == filter.InviteeClubActive.Value);
if (filter.CreatedFrom.HasValue)
query = query.Where(x => x.InvitedAt >= filter.CreatedFrom.Value);
if (filter.CreatedTo.HasValue)
query = query.Where(x => x.InvitedAt <= filter.CreatedTo.Value);
// پیش‌فرض: جدیدترین
query = (request.SortBy?.Trim().ToLowerInvariant()) switch
{
"oldest" or "created_asc" => query.OrderBy(x => x.InvitedAt),
_ => query.OrderByDescending(x => x.InvitedAt)
};
var paginationState = request.PaginationState ?? new PaginationState { PageNumber = 1, PageSize = 20 };
if (paginationState.PageNumber < 1) paginationState.PageNumber = 1;
if (paginationState.PageSize < 1) paginationState.PageSize = 20;
var totalCount = await query.CountAsync(cancellationToken);
var models = await query
.Skip((paginationState.PageNumber - 1) * paginationState.PageSize)
.Take(paginationState.PageSize)
.ToListAsync(cancellationToken);
var totalPage = totalCount == 0
? 0
: (int)Math.Ceiling((double)totalCount / paginationState.PageSize);
return new GetInvitationsByFilterResult
{
MetaData = new MetaData
{
TotalCount = totalCount,
CurrentPage = paginationState.PageNumber,
PageSize = paginationState.PageSize,
TotalPage = totalPage,
HasPrevious = paginationState.PageNumber > 1,
HasNext = paginationState.PageNumber < totalPage
},
Models = models
};
}
}
@@ -1,36 +0,0 @@
using CMSMicroservice.Application.Common.Models;
using MediatR;
namespace CMSMicroservice.Application.UserCQ.Queries.GetMyInvitees;
public class GetMyInviteesQuery : IRequest<GetMyInviteesResult>
{
public long UserId { get; set; }
public PaginationState? PaginationState { get; set; }
/// <summary>ALL | ACTIVE | INACTIVE — ClubMembership.IsActive</summary>
public string? ClubStatusFilter { get; set; }
}
public class GetMyInviteesResult
{
public MetaData MetaData { get; set; } = new();
public List<InviteeDto> Invitees { get; set; } = new();
public InviteeStatsDto Stats { get; set; } = new();
}
public class InviteeDto
{
public long UserId { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string Mobile { get; set; } = string.Empty;
public DateTime RegisteredAt { get; set; }
public bool IsClubActive { get; set; }
}
public class InviteeStatsDto
{
public int TotalInvitees { get; set; }
public int ClubActiveCount { get; set; }
public int ClubInactiveCount { get; set; }
}
@@ -1,98 +0,0 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.UserCQ.Queries.GetMyInvitees;
public class GetMyInviteesQueryHandler : IRequestHandler<GetMyInviteesQuery, GetMyInviteesResult>
{
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser;
public GetMyInviteesQueryHandler(IApplicationDbContext context, ICurrentUserService currentUser)
{
_context = context;
_currentUser = currentUser;
}
public async Task<GetMyInviteesResult> Handle(GetMyInviteesQuery request, CancellationToken cancellationToken)
{
var userId = request.UserId == 0
? (long.TryParse(_currentUser.UserId, out var id) ? id : 0)
: request.UserId;
if (userId == 0)
throw new UnauthorizedAccessException("User ID not found in JWT token");
var baseQuery = _context.UserInvitations
.AsNoTracking()
.Where(x => x.InviterUserId == userId)
.Select(x => new
{
x.InviteeUserId,
InviteeFirstName = x.Invitee.FirstName,
InviteeLastName = x.Invitee.LastName,
InviteeMobile = x.Invitee.Mobile,
RegisteredAt = x.Invitee.Created,
IsClubActive = x.Invitee.ClubMembership != null && x.Invitee.ClubMembership.IsActive,
InvitedAt = x.Created
});
var all = await baseQuery.ToListAsync(cancellationToken);
var total = all.Count;
var clubActive = all.Count(x => x.IsClubActive);
var clubInactive = total - clubActive;
var filtered = all.AsEnumerable();
var status = request.ClubStatusFilter?.Trim().ToUpperInvariant();
if (status == "ACTIVE")
filtered = filtered.Where(x => x.IsClubActive);
else if (status == "INACTIVE")
filtered = filtered.Where(x => !x.IsClubActive);
var ordered = filtered.OrderByDescending(x => x.InvitedAt).ToList();
var paginationState = request.PaginationState ?? new PaginationState { PageNumber = 1, PageSize = 10 };
if (paginationState.PageNumber < 1) paginationState.PageNumber = 1;
if (paginationState.PageSize < 1) paginationState.PageSize = 10;
var totalCount = ordered.Count;
var pageItems = ordered
.Skip((paginationState.PageNumber - 1) * paginationState.PageSize)
.Take(paginationState.PageSize)
.ToList();
var totalPage = totalCount == 0
? 0
: (int)Math.Ceiling((double)totalCount / paginationState.PageSize);
return new GetMyInviteesResult
{
MetaData = new MetaData
{
TotalCount = totalCount,
CurrentPage = paginationState.PageNumber,
PageSize = paginationState.PageSize,
TotalPage = totalPage,
HasPrevious = paginationState.PageNumber > 1,
HasNext = paginationState.PageNumber < totalPage
},
Stats = new InviteeStatsDto
{
TotalInvitees = total,
ClubActiveCount = clubActive,
ClubInactiveCount = clubInactive
},
Invitees = pageItems.Select(x => new InviteeDto
{
UserId = x.InviteeUserId,
FirstName = x.InviteeFirstName,
LastName = x.InviteeLastName,
Mobile = x.InviteeMobile,
RegisteredAt = x.RegisteredAt,
IsClubActive = x.IsClubActive
}).ToList()
};
}
}
@@ -1,4 +1,3 @@
using CMSMicroservice.Application.Common;
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
@@ -51,10 +50,8 @@ public class GetCustomerOrderQueryHandler : IRequestHandler<GetCustomerOrderQuer
DeliveryStatus = order.DeliveryStatus,
TrackingCode = order.TrackingCode ?? "",
DeliveryDescription = order.DeliveryDescription ?? "",
UserFullName = UserDisplayName.From(order.User),
UserFullName = $"{order.User?.FirstName ?? ""} {order.User?.LastName ?? ""}".Trim(),
UserNationalCode = order.User?.NationalCode ?? "",
PostalCode = order.UserAddress?.PostalCode ?? "",
UserMobile = order.User?.Mobile ?? "",
VatAmount = order.OrderVAT?.VATAmount ?? 0,
VatPercentage = order.OrderVAT != null ? (double)order.OrderVAT.VATRate * 100 : 0,
FactorDetails = order.FactorDetails?.Select(fd => new FactorDetailDto
@@ -20,8 +20,6 @@ public class GetCustomerOrderResponseDto
public string DeliveryDescription { get; set; }
public string UserFullName { get; set; }
public string UserNationalCode { get; set; }
public string PostalCode { get; set; } = string.Empty;
public string UserMobile { get; set; } = string.Empty;
public long VatAmount { get; set; }
public double VatPercentage { get; set; }
}
@@ -113,7 +113,6 @@ public class GetCustomerOrderHistoryQueryHandler : IRequestHandler<GetCustomerOr
DeliveryStatus.Delivered => 4,
DeliveryStatus.Cancelled => 5,
DeliveryStatus.Returned => 6,
DeliveryStatus.ReadyForOfficePickup => 7,
_ => 0
};
}
@@ -128,7 +127,6 @@ public class GetCustomerOrderHistoryQueryHandler : IRequestHandler<GetCustomerOr
DeliveryStatus.Delivered => "تحویل داده شد",
DeliveryStatus.Cancelled => "لغو شده",
DeliveryStatus.Returned => "مرجوع شده",
DeliveryStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
_ => "نامشخص"
};
}
@@ -1,4 +1,3 @@
using CMSMicroservice.Application.Common;
using CMSMicroservice.Application.Common.Extensions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
@@ -73,7 +72,7 @@ public class GetCustomerOrdersQueryHandler : IRequestHandler<GetCustomerOrdersQu
DeliveryStatus = order.DeliveryStatus,
TrackingCode = order.TrackingCode ?? "",
DeliveryDescription = order.DeliveryDescription ?? "",
UserFullName = UserDisplayName.From(order.User),
UserFullName = $"{order.User?.FirstName ?? ""} {order.User?.LastName ?? ""}".Trim(),
UserNationalCode = order.User?.NationalCode ?? "",
VatAmount = order.OrderVAT?.VATAmount ?? 0,
VatPercentage = order.OrderVAT != null ? (double)order.OrderVAT.VATRate * 100 : 0,
@@ -1,51 +0,0 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetCustomerPackagePurchaseRollup;
public record GetCustomerPackagePurchaseRollupQuery : IRequest<GetCustomerPackagePurchaseRollupResponseDto>
{
public PaginationState? PaginationState { get; init; }
public GetCustomerPackagePurchaseRollupFilter? Filter { get; init; }
}
public class GetCustomerPackagePurchaseRollupFilter
{
public long? UserId { get; set; }
public PackagePurchaseMethod? PurchaseMethod { get; set; }
public DateTime? PurchasedFrom { get; set; }
public DateTime? PurchasedTo { get; set; }
}
public class GetCustomerPackagePurchaseRollupResponseDto
{
public MetaData MetaData { get; set; } = new();
public List<CustomerPackagePurchaseRollupModel> Models { get; set; } = new();
}
public class CustomerPackagePurchaseRollupModel
{
public long UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string UserMobile { get; set; } = string.Empty;
public long FirstPackageId { get; set; }
public string FirstPackageName { get; set; } = string.Empty;
public long FirstAmount { get; set; }
public DateTime FirstPurchasedAt { get; set; }
public long LastPackageId { get; set; }
public string LastPackageName { get; set; } = string.Empty;
public long LastAmount { get; set; }
public DateTime LastPurchasedAt { get; set; }
public PackagePurchaseMethod LastPurchaseMethod { get; set; }
public int PurchaseCount { get; set; }
public long TotalAmount { get; set; }
public int DayaCount { get; set; }
public long DayaAmount { get; set; }
public int ManualCount { get; set; }
public long ManualAmount { get; set; }
public int GatewayCount { get; set; }
public long GatewayAmount { get; set; }
}
@@ -1,136 +0,0 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Enums;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetCustomerPackagePurchaseRollup;
public class GetCustomerPackagePurchaseRollupQueryHandler
: IRequestHandler<GetCustomerPackagePurchaseRollupQuery, GetCustomerPackagePurchaseRollupResponseDto>
{
private readonly IApplicationDbContext _context;
public GetCustomerPackagePurchaseRollupQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetCustomerPackagePurchaseRollupResponseDto> Handle(
GetCustomerPackagePurchaseRollupQuery request,
CancellationToken cancellationToken)
{
var filter = request.Filter;
var pagination = request.PaginationState ?? new PaginationState { PageNumber = 1, PageSize = 20 };
if (pagination.PageNumber < 1) pagination.PageNumber = 1;
// PageSize == 0 → خروجی کامل (بدون صفحه‌بندی)، مثلاً Excel
var exportAll = pagination.PageSize == 0;
if (!exportAll && pagination.PageSize < 1) pagination.PageSize = 20;
// خرید معتبر: بدون تراکنش یا تراکنش موفق
var purchases = _context.UserPackagePurchases
.AsNoTracking()
.Include(x => x.User)
.Include(x => x.Package)
.Include(x => x.Transaction)
.Where(x => x.TransactionId == null
|| (x.Transaction != null && x.Transaction.PaymentStatus == PaymentStatus.Success));
if (filter?.UserId is > 0)
purchases = purchases.Where(x => x.UserId == filter.UserId.Value);
if (filter?.PurchaseMethod != null)
purchases = purchases.Where(x => x.PurchaseMethod == filter.PurchaseMethod.Value);
if (filter?.PurchasedFrom != null)
purchases = purchases.Where(x => x.PurchasedAt >= filter.PurchasedFrom.Value);
if (filter?.PurchasedTo != null)
purchases = purchases.Where(x => x.PurchasedAt <= filter.PurchasedTo.Value);
var rows = await purchases
.Select(x => new
{
x.UserId,
FirstName = x.User != null ? x.User.FirstName : null,
LastName = x.User != null ? x.User.LastName : null,
Mobile = x.User != null ? x.User.Mobile : null,
x.PackageId,
PackageName = x.Package != null ? x.Package.Title : null,
x.Amount,
x.PurchasedAt,
x.PurchaseMethod
})
.ToListAsync(cancellationToken);
var grouped = rows
.GroupBy(x => x.UserId)
.Select(g =>
{
var ordered = g.OrderBy(x => x.PurchasedAt).ThenBy(x => x.PackageId).ToList();
var first = ordered.First();
var last = ordered.Last();
var userName = string.Join(" ",
new[] { first.FirstName, first.LastName }.Where(s => !string.IsNullOrWhiteSpace(s))).Trim();
return new CustomerPackagePurchaseRollupModel
{
UserId = g.Key,
UserName = string.IsNullOrWhiteSpace(userName)
? (!string.IsNullOrWhiteSpace(first.Mobile) ? first.Mobile! : $"کاربر {g.Key}")
: userName,
UserMobile = first.Mobile ?? string.Empty,
FirstPackageId = first.PackageId,
FirstPackageName = first.PackageName ?? string.Empty,
FirstAmount = first.Amount,
FirstPurchasedAt = first.PurchasedAt,
LastPackageId = last.PackageId,
LastPackageName = last.PackageName ?? string.Empty,
LastAmount = last.Amount,
LastPurchasedAt = last.PurchasedAt,
LastPurchaseMethod = last.PurchaseMethod,
PurchaseCount = ordered.Count,
TotalAmount = ordered.Sum(x => x.Amount),
DayaCount = ordered.Count(x => x.PurchaseMethod == PackagePurchaseMethod.DayaLoan),
DayaAmount = ordered.Where(x => x.PurchaseMethod == PackagePurchaseMethod.DayaLoan).Sum(x => x.Amount),
ManualCount = ordered.Count(x => x.PurchaseMethod == PackagePurchaseMethod.Manual),
ManualAmount = ordered.Where(x => x.PurchaseMethod == PackagePurchaseMethod.Manual).Sum(x => x.Amount),
GatewayCount = ordered.Count(x => x.PurchaseMethod == PackagePurchaseMethod.DirectPurchase),
GatewayAmount = ordered.Where(x => x.PurchaseMethod == PackagePurchaseMethod.DirectPurchase).Sum(x => x.Amount)
};
})
.OrderByDescending(x => x.LastPurchasedAt)
.ToList();
var totalCount = grouped.Count;
var pageNumber = exportAll ? 1 : pagination.PageNumber;
var pageSize = exportAll ? totalCount : pagination.PageSize;
var totalPage = exportAll || pageSize <= 0
? (totalCount > 0 ? 1 : 0)
: (int)Math.Ceiling(totalCount / (double)pageSize);
var pageModels = exportAll
? grouped
: grouped
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToList();
return new GetCustomerPackagePurchaseRollupResponseDto
{
MetaData = new MetaData
{
CurrentPage = pageNumber,
PageSize = pageSize,
TotalCount = totalCount,
TotalPage = totalPage,
HasPrevious = !exportAll && pageNumber > 1,
HasNext = !exportAll && pageNumber < totalPage
},
Models = pageModels
};
}
}
@@ -1,18 +0,0 @@
namespace CMSMicroservice.Domain.Entities;
/// <summary>
/// رابطه دعوت‌کننده ↔ دعوت‌شونده (جدا از معرف شبکه / NetworkParent).
/// کد دعوت = شماره موبایل دعوت‌کننده.
/// </summary>
public class UserInvitation : BaseAuditableEntity
{
/// <summary>کاربری که دعوت کرده</summary>
public long InviterUserId { get; set; }
public virtual User Inviter { get; set; } = null!;
/// <summary>کاربری که دعوت شده (حداکثر یک دعوت‌کننده)</summary>
public long InviteeUserId { get; set; }
public virtual User Invitee { get; set; } = null!;
}
@@ -15,7 +15,5 @@ public enum DeliveryStatus
Returned = 4,
// لغو شده
Cancelled = 5,
// آماده تحویل حضوری در دفتر
ReadyForOfficePickup = 6,
}
@@ -18,10 +18,5 @@ public enum PackagePurchaseMethod
/// <summary>
/// از طریق پرداخت مستقیم درگاه بانکی
/// </summary>
DirectPurchase = 2,
/// <summary>
/// ثبت دستی توسط ادمین
/// </summary>
Manual = 3
DirectPurchase = 2
}
@@ -77,33 +77,27 @@ public static class ConfigureServices
});
}
// Payment Gateway — default provider + multi-gateway resolver (ZarinPal + TwentyCard coexist)
// Payment Gateway Service - Multi-Provider Architecture
// پشتیبانی از درگاه‌های مختلف: ZarinPal, Daya, Mock
var paymentProvider = configuration.GetValue<string>("PaymentProvider", "Mock")?.ToLowerInvariant();
services.AddHttpClient<ZarinPalPaymentService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
services.AddHttpClient<TwentyCardPaymentService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
switch (paymentProvider)
{
case "zarinpal":
services.AddScoped<IPaymentGatewayService>(sp => sp.GetRequiredService<ZarinPalPaymentService>());
break;
case "daya":
services.AddHttpClient<DayaPaymentService>()
services.AddHttpClient<IPaymentGatewayService, ZarinPalPaymentService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
services.AddScoped<IPaymentGatewayService>(sp => sp.GetRequiredService<DayaPaymentService>());
break;
case "daya":
services.AddHttpClient<IPaymentGatewayService, DayaPaymentService>()
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
break;
case "mock":
default:
services.AddScoped<IPaymentGatewayService, MockPaymentGatewayService>();
break;
}
services.AddScoped<IPaymentGatewayResolver, PaymentGatewayResolver>();
services.AddScoped<IApplicationDbContext>(p => p.GetRequiredService<ApplicationDbContext>());
@@ -86,7 +86,6 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
public DbSet<Product> Products => Set<Product>();
public DbSet<ProductImage> ProductImages => Set<ProductImage>();
public DbSet<User> Users => Set<User>();
public DbSet<UserInvitation> UserInvitations => Set<UserInvitation>();
public DbSet<OtpToken> OtpTokens => Set<OtpToken>();
public DbSet<Contract> Contracts => Set<Contract>();
public DbSet<UserContract> UserContracts => Set<UserContract>();
@@ -13,7 +13,7 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
builder.Property(entity => entity.Id).UseIdentityColumn();
builder.Property(entity => entity.FirstName).IsRequired(false);
builder.Property(entity => entity.LastName).IsRequired(false);
builder.Property(entity => entity.Mobile).IsRequired(true).HasMaxLength(20);
builder.Property(entity => entity.Mobile).IsRequired(true);
builder.Property(entity => entity.NationalCode).IsRequired(false);
builder.Property(entity => entity.AvatarPath).IsRequired(false);
builder.Property(entity => entity.ReferralCode).IsRequired(true);
@@ -48,10 +48,5 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
builder.HasIndex(e => e.LegPosition)
.HasDatabaseName("IX_User_LegPosition");
// موبایل یکتا برای کاربران فعال (lookup دعوت‌کننده)
builder.HasIndex(e => e.Mobile)
.IsUnique()
.HasFilter("[IsDeleted] = 0")
.HasDatabaseName("IX_User_Mobile_Active");
}
}
@@ -1,38 +0,0 @@
using CMSMicroservice.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
public class UserInvitationConfiguration : IEntityTypeConfiguration<UserInvitation>
{
public void Configure(EntityTypeBuilder<UserInvitation> builder)
{
builder.ToTable("UserInvitations");
builder.HasQueryFilter(p => !p.IsDeleted);
builder.Ignore(entity => entity.DomainEvents);
builder.HasKey(entity => entity.Id);
builder.Property(entity => entity.Id).UseIdentityColumn();
builder.Property(entity => entity.InviterUserId).IsRequired();
builder.Property(entity => entity.InviteeUserId).IsRequired();
builder.HasOne(entity => entity.Inviter)
.WithMany()
.HasForeignKey(entity => entity.InviterUserId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(entity => entity.Invitee)
.WithMany()
.HasForeignKey(entity => entity.InviteeUserId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(e => e.InviteeUserId)
.IsUnique()
.HasDatabaseName("IX_UserInvitations_InviteeUserId");
builder.HasIndex(e => new { e.InviterUserId, e.Created })
.HasDatabaseName("IX_UserInvitations_InviterUserId_Created");
}
}
@@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260222160755_u21")]
[Migration("20260222155925_u21")]
partial class u21
{
/// <inheritdoc />
@@ -1,103 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddUserInvitations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Mobile",
schema: "CMS",
table: "Users",
type: "nvarchar(20)",
maxLength: 20,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)");
migrationBuilder.CreateTable(
name: "UserInvitations",
schema: "CMS",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
InviterUserId = table.Column<long>(type: "bigint", nullable: false),
InviteeUserId = table.Column<long>(type: "bigint", nullable: false),
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserInvitations", x => x.Id);
table.ForeignKey(
name: "FK_UserInvitations_Users_InviteeUserId",
column: x => x.InviteeUserId,
principalSchema: "CMS",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_UserInvitations_Users_InviterUserId",
column: x => x.InviterUserId,
principalSchema: "CMS",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_User_Mobile_Active",
schema: "CMS",
table: "Users",
column: "Mobile",
unique: true,
filter: "[IsDeleted] = 0");
migrationBuilder.CreateIndex(
name: "IX_UserInvitations_InviteeUserId",
schema: "CMS",
table: "UserInvitations",
column: "InviteeUserId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_UserInvitations_InviterUserId_Created",
schema: "CMS",
table: "UserInvitations",
columns: new[] { "InviterUserId", "Created" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserInvitations",
schema: "CMS");
migrationBuilder.DropIndex(
name: "IX_User_Mobile_Active",
schema: "CMS",
table: "Users");
migrationBuilder.AlterColumn<string>(
name: "Mobile",
schema: "CMS",
table: "Users",
type: "nvarchar(max)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(20)",
oldMaxLength: 20);
}
}
}
@@ -3574,7 +3574,7 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<string>("Mobile")
.IsRequired()
.HasColumnType("nvarchar(450)");
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("MobileVerifiedAt")
.HasColumnType("datetime2");
@@ -3606,11 +3606,6 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.HasIndex("LegPosition")
.HasDatabaseName("IX_User_LegPosition");
b.HasIndex("Mobile")
.IsUnique()
.HasDatabaseName("IX_User_Mobile_Active")
.HasFilter("[IsDeleted] = 0");
b.HasIndex("NetworkParentId")
.HasDatabaseName("IX_User_NetworkParentId");
@@ -3755,47 +3750,6 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.ToTable("UserContracts", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserInvitation", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<long>("InviteeUserId")
.HasColumnType("bigint");
b.Property<long>("InviterUserId")
.HasColumnType("bigint");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTime?>("LastModified")
.HasColumnType("datetime2");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("InviteeUserId")
.IsUnique()
.HasDatabaseName("IX_UserInvitations_InviteeUserId");
b.HasIndex("InviterUserId", "Created")
.HasDatabaseName("IX_UserInvitations_InviterUserId_Created");
b.ToTable("UserInvitations", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b =>
{
b.Property<long>("Id")
@@ -4917,25 +4871,6 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Navigation("User");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserInvitation", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.User", "Invitee")
.WithMany()
.HasForeignKey("InviteeUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CMSMicroservice.Domain.Entities.User", "Inviter")
.WithMany()
.HasForeignKey("InviterUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Invitee");
b.Navigation("Inviter");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
@@ -0,0 +1,136 @@
-- =============================================
-- Stored Procedure: GetNetworkTree
-- Description: دریافت درخت شبکه باینری با CTE recursive
-- Author: FourSat Team
-- Updated: 2026-04-30 — اضافه کردن IsNewActivation، PackageName، PackageId
-- منطق هفته‌بندی از ActivatedAt به PackagePurchasedAt تغییر کرد
-- =============================================
CREATE OR ALTER PROCEDURE [CMS].[GetNetworkTree]
@RootUserId BIGINT,
@MaxDepth INT = 100,
@IsClubActive BIT = NULL,
@ActivationWeekDefinitionId BIGINT = NULL
AS
BEGIN
SET NOCOUNT ON;
-- تاریخ شروع و پایان هفته فیلتر
DECLARE @WeekStartDate DATETIME = NULL;
DECLARE @WeekEndDate DATETIME = NULL;
IF @ActivationWeekDefinitionId IS NOT NULL
BEGIN
SELECT @WeekStartDate = StartDate, @WeekEndDate = EndDate
FROM [CMS].[WeekDefinitions]
WHERE Id = @ActivationWeekDefinitionId;
END
-- CTE برای پیمایش درخت باینری به صورت recursive
;WITH NetworkTreeCTE AS (
-- Base case: ریشه درخت
SELECT
u.Id AS UserId,
u.Mobile,
u.FirstName,
u.LastName,
u.ReferralCode,
u.LegPosition,
u.NetworkParentId AS ParentId,
0 AS NetworkLevel,
u.Created AS UserCreated
FROM [CMS].[Users] u
WHERE u.Id = @RootUserId
AND u.IsDeleted = 0
UNION ALL
-- Recursive case: فرزندان
SELECT
u.Id AS UserId,
u.Mobile,
u.FirstName,
u.LastName,
u.ReferralCode,
u.LegPosition,
u.NetworkParentId AS ParentId,
parent.NetworkLevel + 1 AS NetworkLevel,
u.Created AS UserCreated
FROM [CMS].[Users] u
INNER JOIN NetworkTreeCTE parent ON u.NetworkParentId = parent.UserId
WHERE u.IsDeleted = 0
AND parent.NetworkLevel < @MaxDepth
)
SELECT
t.UserId,
t.Mobile,
t.FirstName,
t.LastName,
t.ReferralCode,
t.LegPosition,
t.ParentId,
t.NetworkLevel,
cm.ActivatedAt AS ClubActivatedAt,
ISNULL(cm.IsActive, 0) AS IsClubActive,
-- هفته فعال‌سازی بر اساس Cycle جاری (PackagePurchasedAt)
wd.Id AS ActivationWeekDefinitionId,
wd.DisplayName AS ActivationWeekDisplayName,
-- آیا کاربر در هفته هدف Cycle داشته؟
CASE WHEN cc_target.UserId IS NOT NULL THEN 1 ELSE 0 END AS IsActivatedInTargetWeek,
-- نوع فعال‌سازی: 1=جدید، 0=تمدید/خرید مجدد، NULL=بدون Cycle
CASE
WHEN cc_eff.CycleNumber IS NULL THEN NULL
WHEN cc_eff.CycleNumber = 1 THEN 1
ELSE 0
END AS IsNewActivation,
-- نام پکیج موثر (هفته هدف اگر فیلتر باشد، وگرنه پکیج فعلی)
ISNULL(p_eff.Title, '') AS PackageName,
cc_eff.PackageId AS PackageId,
t.UserCreated
FROM NetworkTreeCTE t
LEFT JOIN [CMS].[ClubMemberships] cm ON cm.UserId = t.UserId AND cm.IsDeleted = 0
-- Cycle جاری — برای نمایش هفته و پکیج وقتی فیلتر هفته نیست
OUTER APPLY (
SELECT TOP 1 CycleNumber, PackageId, PackagePurchasedAt
FROM [CMS].[ClubMembershipCycles]
WHERE UserId = t.UserId AND IsCurrentCycle = 1
) cc_current
-- Cycle هفته هدف — برای IsActivatedInTargetWeek و نوع فعال‌سازی
OUTER APPLY (
SELECT TOP 1 UserId, CycleNumber, PackageId
FROM [CMS].[ClubMembershipCycles]
WHERE UserId = t.UserId
AND @WeekStartDate IS NOT NULL
AND PackagePurchasedAt >= @WeekStartDate
AND PackagePurchasedAt < @WeekEndDate
ORDER BY CycleNumber ASC
) cc_target
-- Cycle موثر: اگر هفته فیلتر داریم → cc_target، وگرنه → cc_current
CROSS APPLY (
SELECT
CASE WHEN @WeekStartDate IS NOT NULL AND cc_target.UserId IS NOT NULL
THEN cc_target.CycleNumber ELSE cc_current.CycleNumber END AS CycleNumber,
CASE WHEN @WeekStartDate IS NOT NULL AND cc_target.UserId IS NOT NULL
THEN cc_target.PackageId ELSE cc_current.PackageId END AS PackageId
) cc_eff
-- پکیج موثر
LEFT JOIN [CMS].[Packages] p_eff ON p_eff.Id = cc_eff.PackageId AND p_eff.IsDeleted = 0
-- هفته بر اساس PackagePurchasedAt سایکل جاری
LEFT JOIN [CMS].[WeekDefinitions] wd
ON cc_current.PackagePurchasedAt >= wd.StartDate
AND cc_current.PackagePurchasedAt < wd.EndDate
WHERE (@IsClubActive IS NULL OR ISNULL(cm.IsActive, 0) = @IsClubActive OR t.NetworkLevel = 0)
ORDER BY t.NetworkLevel, t.ParentId, t.LegPosition
OPTION (MAXRECURSION 0);
END
@@ -1,6 +1,5 @@
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;
@@ -14,12 +13,10 @@ namespace CMSMicroservice.Infrastructure.Services;
public class GenerateJwtTokenService : IGenerateJwtToken
{
private readonly IConfiguration _configuration;
private readonly IApplicationDbContext _context;
public GenerateJwtTokenService(IConfiguration configuration, IApplicationDbContext context)
public GenerateJwtTokenService(IConfiguration configuration)
{
_configuration = configuration;
_context = context;
}
public async Task<string> GenerateJwtToken(User user, int? expiryDays = null)
@@ -61,10 +58,6 @@ 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)
@@ -1,35 +0,0 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Infrastructure.Services.Payment;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CMSMicroservice.Infrastructure.Services.Payment;
public class PaymentGatewayResolver : IPaymentGatewayResolver
{
private readonly IServiceProvider _serviceProvider;
private readonly IConfiguration _configuration;
public PaymentGatewayResolver(IServiceProvider serviceProvider, IConfiguration configuration)
{
_serviceProvider = serviceProvider;
_configuration = configuration;
}
public IPaymentGatewayService Get(string? provider)
{
var key = Normalize(provider)
?? Normalize(_configuration["PaymentProvider"])
?? "zarinpal";
return key switch
{
"20card" or "twentycard" => _serviceProvider.GetRequiredService<TwentyCardPaymentService>(),
"zarinpal" => _serviceProvider.GetRequiredService<ZarinPalPaymentService>(),
_ => _serviceProvider.GetRequiredService<IPaymentGatewayService>()
};
}
private static string? Normalize(string? value)
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant();
}
@@ -1,306 +0,0 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using CMSMicroservice.Application.Common.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Infrastructure.Services.Payment;
/// <summary>
/// درگاه پرداخت بیست‌کارت (20Card IPG Merchant API).
/// مبلغ ورودی به تومان است. درخواست‌ها به‌صورت multipart/form-data ارسال می‌شوند.
/// </summary>
public class TwentyCardPaymentService : IPaymentGatewayService
{
private readonly HttpClient _httpClient;
private readonly ILogger<TwentyCardPaymentService> _logger;
private readonly string _terminalId;
private readonly string _terminalUsername;
private readonly string _terminalPassword;
private readonly string _defaultEmail;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
public TwentyCardPaymentService(
HttpClient httpClient,
IConfiguration configuration,
ILogger<TwentyCardPaymentService> logger)
{
_httpClient = httpClient;
_logger = logger;
var baseUrl = configuration["TwentyCard:BaseUrl"]
?? throw new InvalidOperationException("TwentyCard:BaseUrl is not configured.");
_terminalId = configuration["TwentyCard:TerminalId"]
?? throw new InvalidOperationException("TwentyCard:TerminalId is not configured.");
_terminalUsername = configuration["TwentyCard:TerminalUsername"]
?? throw new InvalidOperationException("TwentyCard:TerminalUsername is not configured.");
_terminalPassword = configuration["TwentyCard:TerminalPassword"]
?? throw new InvalidOperationException("TwentyCard:TerminalPassword is not configured.");
_defaultEmail = configuration["TwentyCard:DefaultEmail"] ?? "info@kbs2.ir";
_httpClient.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
if (!_httpClient.DefaultRequestHeaders.Contains("Accept"))
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/json");
_logger.LogInformation("TwentyCard payment service initialized. BaseUrl={BaseUrl}", baseUrl);
}
public async Task<PaymentInitiateResult> InitiatePaymentAsync(
PaymentRequest request,
CancellationToken cancellationToken = default)
{
try
{
// ۲۰کارت amount را به ریال می‌گیرد (تومان × ۱۰)
var amountRial = ((long)request.Amount) * 10L;
var amount = amountRial.ToString();
var payerName = string.IsNullOrWhiteSpace(request.Name)
? (string.IsNullOrWhiteSpace(request.Mobile) ? "کاربر" : request.Mobile)
: request.Name.Trim();
var invoice = string.IsNullOrWhiteSpace(request.Invoice)
? request.UserId.ToString()
: request.Invoice;
using var content = BuildForm(new Dictionary<string, string>
{
["terminal_id"] = _terminalId,
["terminal_username"] = _terminalUsername,
["terminal_password"] = _terminalPassword,
["amount"] = amount,
["name"] = payerName,
["mobile"] = request.Mobile ?? string.Empty,
["invoice"] = invoice,
["callback"] = request.CallbackUrl,
["description"] = request.Description ?? string.Empty,
// بدون email درگاه ۲۰کارت به‌جای validation، HTTP 500 برمی‌گرداند
["email"] = ResolveEmail(request.Email)
});
if (!string.IsNullOrWhiteSpace(request.NationalId))
AddFormField(content, "national_id", request.NationalId);
_logger.LogInformation(
"TwentyCard payment request: Amount={AmountToman} Toman ({AmountRial} Rial), User={UserId}, Invoice={Invoice}",
(long)request.Amount, amountRial, request.UserId, invoice);
var response = await _httpClient.PostAsync("api/pgw/merchant/request", content, cancellationToken);
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
if (!response.IsSuccessStatusCode)
{
_logger.LogError(
"TwentyCard HTTP error on request: StatusCode={StatusCode}, Body={Body}",
(int)response.StatusCode, responseBody);
return new PaymentInitiateResult
{
IsSuccess = false,
ErrorMessage = $"خطای ارتباط با درگاه ۲۰کارت (HTTP {(int)response.StatusCode})"
};
}
var result = JsonSerializer.Deserialize<TwentyCardRequestResponse>(responseBody, JsonOptions);
var authority = result?.Data?.Authority;
var redirectUrl = result?.Data?.RedirectUrl;
if (string.IsNullOrWhiteSpace(authority) || string.IsNullOrWhiteSpace(redirectUrl))
{
_logger.LogError("TwentyCard request failed. Body={Body}", responseBody);
return new PaymentInitiateResult
{
IsSuccess = false,
ErrorMessage = result?.Message
?? result?.Error?.Message
?? "ایجاد تراکنش ۲۰کارت ناموفق بود"
};
}
return new PaymentInitiateResult
{
IsSuccess = true,
RefId = authority,
GatewayUrl = redirectUrl
};
}
catch (Exception ex)
{
_logger.LogError(ex, "TwentyCard InitiatePayment failed");
return new PaymentInitiateResult
{
IsSuccess = false,
ErrorMessage = "خطا در ارتباط با درگاه ۲۰کارت"
};
}
}
public Task<PaymentVerificationResult> VerifyPaymentAsync(
string refId,
string verificationToken,
CancellationToken cancellationToken = default)
=> throw new NotImplementedException("TwentyCard requires VerifyPaymentAsync with amount.");
public async Task<PaymentVerificationResult> VerifyPaymentAsync(
string refId,
string verificationToken,
decimal amountInToman,
CancellationToken cancellationToken = default)
{
try
{
if (!string.Equals(verificationToken, "OK", StringComparison.OrdinalIgnoreCase))
{
return new PaymentVerificationResult
{
IsSuccess = false,
RefId = refId,
Message = "پرداخت توسط کاربر لغو شد یا ناموفق بود",
VerificationCode = 0
};
}
using var content = BuildForm(new Dictionary<string, string>
{
["terminal_id"] = _terminalId,
["terminal_username"] = _terminalUsername,
["terminal_password"] = _terminalPassword,
["authority"] = refId,
["amount"] = (((long)amountInToman) * 10L).ToString()
});
var response = await _httpClient.PostAsync("api/pgw/merchant/verify", content, cancellationToken);
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogInformation(
"TwentyCard verify response: Authority={Authority}, StatusCode={StatusCode}, Body={Body}",
refId, (int)response.StatusCode, responseBody);
var result = JsonSerializer.Deserialize<TwentyCardVerifyResponse>(responseBody, JsonOptions);
var paid = result?.Status?.Paid == true;
var verified = result?.Status?.Verified == true;
var noError = result?.Error != true;
if (response.IsSuccessStatusCode && noError && paid && verified)
{
return new PaymentVerificationResult
{
IsSuccess = true,
RefId = refId,
TrackingCode = refId,
Message = result?.Message ?? "تراکنش با موفقیت تایید شد",
VerificationCode = result?.Code ?? 200
};
}
// Code 103 = already verified — treat as success for idempotency
if (result?.Code == 103)
{
return new PaymentVerificationResult
{
IsSuccess = true,
RefId = refId,
TrackingCode = refId,
Message = result.Message ?? "تراکنش قبلاً تایید شده است",
VerificationCode = 103
};
}
return new PaymentVerificationResult
{
IsSuccess = false,
RefId = refId,
Message = result?.Message ?? "تایید تراکنش ۲۰کارت ناموفق بود",
VerificationCode = result?.Code ?? (int)response.StatusCode
};
}
catch (Exception ex)
{
_logger.LogError(ex, "TwentyCard VerifyPayment failed. Authority={Authority}", refId);
return new PaymentVerificationResult
{
IsSuccess = false,
RefId = refId,
Message = "خطا در تایید تراکنش ۲۰کارت"
};
}
}
public Task<PayoutResult> ProcessPayoutAsync(
PayoutRequest request,
CancellationToken cancellationToken = default)
{
return Task.FromResult(new PayoutResult
{
IsSuccess = false,
Message = "واریز از طریق درگاه ۲۰کارت پشتیبانی نمی‌شود"
});
}
private static MultipartFormDataContent BuildForm(Dictionary<string, string> fields)
{
var form = new MultipartFormDataContent();
foreach (var (key, value) in fields)
AddFormField(form, key, value ?? string.Empty);
return form;
}
/// <summary>
/// شبیه curl --form: بدون Content-Type روی هر part ( بعضی بک‌اندهای PHP با charset=utf-8 می‌شکنند).
/// </summary>
private static void AddFormField(MultipartFormDataContent form, string name, string value)
{
var part = new StringContent(value ?? string.Empty);
part.Headers.ContentType = null;
form.Add(part, name);
}
private string ResolveEmail(string? email)
{
if (!string.IsNullOrWhiteSpace(email) && email.Contains('@', StringComparison.Ordinal))
return email.Trim();
return _defaultEmail;
}
private sealed class TwentyCardRequestResponse
{
public string? Message { get; set; }
public TwentyCardRequestData? Data { get; set; }
public TwentyCardErrorBody? Error { get; set; }
}
private sealed class TwentyCardErrorBody
{
public string? Code { get; set; }
public string? Message { get; set; }
}
private sealed class TwentyCardRequestData
{
[JsonPropertyName("authority")]
public string? Authority { get; set; }
[JsonPropertyName("redirectUrl")]
public string? RedirectUrl { get; set; }
}
private sealed class TwentyCardVerifyResponse
{
public bool Error { get; set; }
public int Code { get; set; }
public string? Message { get; set; }
public TwentyCardVerifyStatus? Status { get; set; }
}
private sealed class TwentyCardVerifyStatus
{
public bool Paid { get; set; }
public bool Verified { get; set; }
}
}
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.214</Version>
<Version>0.0.205</Version>
<DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
@@ -118,8 +118,6 @@ enum DeliveryStatus
DELIVERY_SHIPPED = 2;
DELIVERY_DELIVERED = 3;
DELIVERY_CANCELLED = 4;
DELIVERY_READY_FOR_OFFICE = 5;
DELIVERY_RETURNED = 6;
}
// Get Order By Id
@@ -209,11 +209,6 @@ message NetworkTreeNodeModel
google.protobuf.BoolValue is_new_activation = 22; // true=اولین فعال‌سازی، false=خرید مجدد، null=بدون Cycle
string package_name = 23; // نام پکیج موثر
google.protobuf.Int64Value package_id = 24; // شناسه پکیج
// تعداد اعضای فعال با پکیج جاری در پای چپ/راست (طلایی و نقره‌ای)
int32 gold_left_leg_total = 25;
int32 gold_right_leg_total = 26;
int32 silver_left_leg_total = 27;
int32 silver_right_leg_total = 28;
}
// GetHistory Query
@@ -315,7 +310,6 @@ message GetMyNetworkTreeResponse
// GetMyNetworkStatistics - for frontend customer display
message GetMyNetworkStatisticsResponse
{
// اعضای فعال باشگاه: کل = چپ + راست
int32 total_members = 1;
int32 active_members = 2;
int32 left_leg_count = 3;
@@ -329,11 +323,6 @@ message GetMyNetworkStatisticsResponse
string my_network_leg = 11;
string my_referral_code = 12;
CustomerLastMemberModel last_member = 13;
// تعداد اعضای فعال با پکیج جاری در پای چپ/راست — نوار آمار پایین شجره (D3)
int32 gold_left_leg_total = 14;
int32 gold_right_leg_total = 15;
int32 silver_left_leg_total = 16;
int32 silver_right_leg_total = 17;
}
message CustomerLastMemberModel
@@ -249,8 +249,6 @@ message CustomerPurchasePackageRequest
int64 package_id = 1;
PurchaseMethodEnum purchase_method = 2;
string callback_url = 3;
// "zarinpal" (default) | "20card"
string gateway_provider = 4;
}
message CustomerPurchasePackageResponse
@@ -66,7 +66,6 @@ message PaymentTransactionModel
google.protobuf.Int64Value transaction_id = 13;
string description = 14;
google.protobuf.Timestamp created_at = 15;
string user_full_name = 16;
}
message GetPaymentTransactionSummaryRequest
@@ -102,10 +102,6 @@ enum DeliveryStatus
DeliveryStatus_Delivered = 3;
// مرجوع شده
DeliveryStatus_Returned = 4;
// لغو شده
DeliveryStatus_Cancelled = 5;
// آماده تحویل حضوری در دفتر
DeliveryStatus_ReadyForOfficePickup = 6;
}
enum TransactionType
{
@@ -192,9 +192,6 @@ 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
@@ -115,16 +115,6 @@ service UserContract
};
};
rpc GetMyInvitees(GetMyInviteesRequest) returns (GetMyInviteesResponse){
option (google.api.http) = {
get: "/Customer/GetMyInvitees"
};
};
rpc GetInvitationsByFilter(GetInvitationsByFilterRequest) returns (GetInvitationsByFilterResponse){
option (google.api.http) = {
get: "/GetInvitationsByFilter"
};
};
rpc UploadCustomerAvatar(UploadCustomerAvatarRequest) returns (UploadCustomerAvatarResponse){
option (google.api.http) = {
post: "/Customer/UploadAvatar"
@@ -248,8 +238,6 @@ 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
{
@@ -304,8 +292,6 @@ message VerifyOtpTokenRequest
string purpose = 2;
string code = 3;
google.protobuf.StringValue parent_referral_code = 4;
// شماره موبایل دعوت‌کننده (اختیاری؛ فقط برای ثبت‌نام جدید)
google.protobuf.StringValue inviter_mobile = 5;
}
message VerifyOtpTokenResponse
{
@@ -478,78 +464,3 @@ message UpdateCustomerSettingsRequest
google.protobuf.StringValue time_zone = 6;
google.protobuf.BoolValue two_factor_auth_enabled = 7;
}
// ============= Inviter (دعوت‌کننده) — جدا از معرف شبکه =============
message GetMyInviteesRequest
{
city.PaginationState pagination_state = 1;
// ALL | ACTIVE | INACTIVE — بر اساس ClubMembership.IsActive
google.protobuf.StringValue club_status_filter = 2;
}
message GetMyInviteesResponse
{
city.MetaData meta_data = 1;
repeated InviteeModel invitees = 2;
InviteeStats stats = 3;
}
message InviteeModel
{
int64 user_id = 1;
google.protobuf.StringValue first_name = 2;
google.protobuf.StringValue last_name = 3;
string mobile = 4;
google.protobuf.Timestamp registered_at = 5;
bool is_club_active = 6;
}
message InviteeStats
{
int32 total_invitees = 1;
int32 club_active_count = 2;
int32 club_inactive_count = 3;
}
message GetInvitationsByFilterRequest
{
city.PaginationState pagination_state = 1;
google.protobuf.StringValue sort_by = 2;
GetInvitationsByFilterFilter filter = 3;
}
message GetInvitationsByFilterFilter
{
google.protobuf.StringValue inviter_mobile = 1;
google.protobuf.StringValue inviter_first_name = 2;
google.protobuf.StringValue inviter_last_name = 3;
google.protobuf.StringValue invitee_mobile = 4;
google.protobuf.StringValue invitee_first_name = 5;
google.protobuf.StringValue invitee_last_name = 6;
google.protobuf.BoolValue invitee_club_active = 7;
google.protobuf.Timestamp created_from = 8;
google.protobuf.Timestamp created_to = 9;
}
message GetInvitationsByFilterResponse
{
city.MetaData meta_data = 1;
repeated InvitationReportModel models = 2;
}
message InvitationReportModel
{
int64 id = 1;
int64 inviter_user_id = 2;
google.protobuf.StringValue inviter_first_name = 3;
google.protobuf.StringValue inviter_last_name = 4;
string inviter_mobile = 5;
int64 invitee_user_id = 6;
google.protobuf.StringValue invitee_first_name = 7;
google.protobuf.StringValue invitee_last_name = 8;
string invitee_mobile = 9;
google.protobuf.Timestamp invitee_registered_at = 10;
bool invitee_club_active = 11;
google.protobuf.Timestamp invited_at = 12;
}
@@ -220,9 +220,6 @@ message GetUserOrderResponse
google.protobuf.StringValue user_national_code = 16;
// اطلاعات مالیات بر ارزش افزوده
OrderVATInfo vat_info = 17;
// اطلاعات پستی برای برچسب کارتن
google.protobuf.StringValue postal_code = 18;
google.protobuf.StringValue user_mobile = 19;
}
// اطلاعات مالیات بر ارزش افزوده
@@ -21,11 +21,6 @@ service UserPackagePurchaseContract
get: "/UserPackagePurchase/GetSummary"
};
};
rpc GetCustomerPackagePurchaseRollup(GetCustomerPackagePurchaseRollupRequest) returns (GetCustomerPackagePurchaseRollupResponse){
option (google.api.http) = {
get: "/UserPackagePurchase/GetCustomerRollup"
};
};
}
message GetAllUserPackagePurchaseByFilterRequest
@@ -78,51 +73,3 @@ message GetUserPackagePurchaseSummaryResponse
int64 total_count = 1;
int64 total_amount = 2;
}
message GetCustomerPackagePurchaseRollupRequest
{
messages.PaginationState pagination_state = 1;
GetCustomerPackagePurchaseRollupFilter filter = 2;
}
message GetCustomerPackagePurchaseRollupFilter
{
google.protobuf.Int64Value user_id = 1;
google.protobuf.Int32Value purchase_method = 2;
google.protobuf.Timestamp purchased_from = 3;
google.protobuf.Timestamp purchased_to = 4;
}
message GetCustomerPackagePurchaseRollupResponse
{
messages.MetaData meta_data = 1;
repeated CustomerPackagePurchaseRollupModel models = 2;
}
message CustomerPackagePurchaseRollupModel
{
int64 user_id = 1;
string user_name = 2;
string user_mobile = 3;
int64 first_package_id = 4;
string first_package_name = 5;
int64 first_amount = 6;
google.protobuf.Timestamp first_purchased_at = 7;
int64 last_package_id = 8;
string last_package_name = 9;
int64 last_amount = 10;
google.protobuf.Timestamp last_purchased_at = 11;
int32 last_purchase_method = 12;
int32 purchase_count = 13;
int64 total_amount = 14;
int32 daya_count = 15;
int64 daya_amount = 16;
int32 manual_count = 17;
int64 manual_amount = 18;
int32 gateway_count = 19;
int64 gateway_amount = 20;
}
@@ -95,11 +95,7 @@ public class NetworkMembershipProfile : IRegister
IsActivatedInTargetWeek = node.IsActivatedInTargetWeek,
PackageName = node.PackageName ?? string.Empty,
PackageId = node.PackageId.HasValue ? node.PackageId.Value : null,
IsNewActivation = node.IsNewActivation.HasValue ? node.IsNewActivation.Value : null,
GoldLeftLegTotal = node.GoldLeftLegTotal,
GoldRightLegTotal = node.GoldRightLegTotal,
SilverLeftLegTotal = node.SilverLeftLegTotal,
SilverRightLegTotal = node.SilverRightLegTotal
IsNewActivation = node.IsNewActivation.HasValue ? node.IsNewActivation.Value : null
};
if (parentId.HasValue)
@@ -46,8 +46,7 @@ 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.UserFullName, src => src.UserFullName ?? string.Empty);
.Map(dest => dest.CreatedAt, src => Timestamp.FromDateTimeOffset(src.Created));
config.NewConfig<ProtoPt.GetPaymentTransactionSummaryRequest, AppPtSummary.GetPaymentTransactionSummaryQuery>()
.Map(dest => dest.Filter, src => src.Filter);
@@ -40,10 +40,7 @@ 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.UserId, src => src.UserId)
.Map(dest => dest.UserFullName, src => src.UserFullName ?? string.Empty)
.Map(dest => dest.Mobile, src => src.Mobile);
.Map(dest => dest.Created, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.Created, DateTimeKind.Utc)));
config.NewConfig<ProtoTx.GetTransactionSummaryRequest, AppTxSummary.GetTransactionSummaryQuery>()
.Map(dest => dest.Filter, src => src.Filter);
@@ -3,7 +3,6 @@ using CMSMicroservice.Domain.Enums;
using Mapster;
using AppUp = CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetAllUserPackagePurchaseByFilter;
using AppUpSummary = CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetUserPackagePurchaseSummary;
using AppRollup = CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetCustomerPackagePurchaseRollup;
using ProtoUp = CMSMicroservice.Protobuf.Protos.UserPackagePurchase;
namespace CMSMicroservice.WebApi.Common.Mappings;
@@ -50,41 +49,5 @@ public class UserPackagePurchaseProfile : IRegister
config.NewConfig<AppUpSummary.GetUserPackagePurchaseSummaryResponseDto, ProtoUp.GetUserPackagePurchaseSummaryResponse>()
.Map(dest => dest.TotalCount, src => src.TotalCount)
.Map(dest => dest.TotalAmount, src => src.TotalAmount);
config.NewConfig<ProtoUp.GetCustomerPackagePurchaseRollupRequest, AppRollup.GetCustomerPackagePurchaseRollupQuery>()
.Map(dest => dest.PaginationState, src => src.PaginationState)
.Map(dest => dest.Filter, src => src.Filter);
config.NewConfig<ProtoUp.GetCustomerPackagePurchaseRollupFilter, AppRollup.GetCustomerPackagePurchaseRollupFilter>()
.Map(dest => dest.UserId, src => src.UserId)
.Map(dest => dest.PurchaseMethod, src => src.PurchaseMethod == null ? null : (PackagePurchaseMethod?)src.PurchaseMethod)
.Map(dest => dest.PurchasedFrom, src => src.PurchasedFrom != null ? src.PurchasedFrom.ToDateTime() : (DateTime?)null)
.Map(dest => dest.PurchasedTo, src => src.PurchasedTo != null ? src.PurchasedTo.ToDateTime() : (DateTime?)null);
config.NewConfig<AppRollup.GetCustomerPackagePurchaseRollupResponseDto, ProtoUp.GetCustomerPackagePurchaseRollupResponse>()
.Map(dest => dest.MetaData, src => src.MetaData)
.Map(dest => dest.Models, src => src.Models);
config.NewConfig<AppRollup.CustomerPackagePurchaseRollupModel, ProtoUp.CustomerPackagePurchaseRollupModel>()
.Map(dest => dest.UserId, src => src.UserId)
.Map(dest => dest.UserName, src => src.UserName)
.Map(dest => dest.UserMobile, src => src.UserMobile)
.Map(dest => dest.FirstPackageId, src => src.FirstPackageId)
.Map(dest => dest.FirstPackageName, src => src.FirstPackageName)
.Map(dest => dest.FirstAmount, src => src.FirstAmount)
.Map(dest => dest.FirstPurchasedAt, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.FirstPurchasedAt, DateTimeKind.Utc)))
.Map(dest => dest.LastPackageId, src => src.LastPackageId)
.Map(dest => dest.LastPackageName, src => src.LastPackageName)
.Map(dest => dest.LastAmount, src => src.LastAmount)
.Map(dest => dest.LastPurchasedAt, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.LastPurchasedAt, DateTimeKind.Utc)))
.Map(dest => dest.LastPurchaseMethod, src => (int)src.LastPurchaseMethod)
.Map(dest => dest.PurchaseCount, src => src.PurchaseCount)
.Map(dest => dest.TotalAmount, src => src.TotalAmount)
.Map(dest => dest.DayaCount, src => src.DayaCount)
.Map(dest => dest.DayaAmount, src => src.DayaAmount)
.Map(dest => dest.ManualCount, src => src.ManualCount)
.Map(dest => dest.ManualAmount, src => src.ManualAmount)
.Map(dest => dest.GatewayCount, src => src.GatewayCount)
.Map(dest => dest.GatewayAmount, src => src.GatewayAmount);
}
}
@@ -4,10 +4,7 @@ public class UserProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
config.NewConfig<Application.UserCQ.Queries.GetAllUserByFilter.GetAllUserByFilterResponseModel,
CMSMicroservice.Protobuf.Protos.User.GetAllUserByFilterResponseModel>()
.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);
//config.NewConfig<Source,Destination>()
// .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}");
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM 185.252.31.44:32082/dotnet/aspnet:9.0 AS base
FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM 185.252.31.44:32082/dotnet/sdk:9.0 AS build
FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj", "CMSMicroservice.WebApi/"]
COPY ["CMSMicroservice.Application/CMSMicroservice.Application.csproj", "CMSMicroservice.Application/"]
@@ -99,20 +99,7 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB
public override async Task<UpdateOrderStatusResponse> UpdateOrderStatus(UpdateOrderStatusRequest request, ServerCallContext context)
{
// Proto DeliveryStatus ≠ Domain DeliveryStatus — نگاشت صریح لازم است
var command = new UpdateOrderStatusCommand
{
OrderId = request.OrderId,
DeliveryStatus = MapProtoDeliveryToDomain(request.DeliveryStatus),
TrackingCode = request.TrackingCode,
AdminNotes = request.AdminNotes
};
var result = await _sender.Send(command, context.CancellationToken);
return new UpdateOrderStatusResponse
{
Success = result.Success,
Message = result.Message ?? string.Empty
};
return await _dispatchRequestToCQRS.Handle<UpdateOrderStatusRequest, UpdateOrderStatusCommand, UpdateOrderStatusResponse>(request, context);
}
public override async Task<GetOrderByIdResponse> GetOrderById(GetOrderByIdRequest request, ServerCallContext context)
@@ -154,8 +141,6 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB
Address = result.Address.Address ?? "",
PostalCode = result.Address.PostalCode ?? ""
};
if (!string.IsNullOrWhiteSpace(result.Address.Phone))
response.Address.Phone = result.Address.Phone;
}
foreach (var item in result.Items)
@@ -212,98 +197,9 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB
return response;
}
public override async Task<GetAllDiscountOrdersResponse> GetAllDiscountOrders(
GetAllDiscountOrdersRequest request, ServerCallContext context)
public override async Task<GetAllDiscountOrdersResponse> GetAllDiscountOrders(GetAllDiscountOrdersRequest request, ServerCallContext context)
{
// Proto PaymentStatus ≠ Domain PaymentStatus (مقادیر int متفاوت) — Adapt خام باعث باگ لیست/فیلتر می‌شود
if (request.PaymentStatus is 3) // PAYMENT_REFUNDED — در دامنه وجود ندارد
{
return new GetAllDiscountOrdersResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
TotalCount = 0,
PageSize = request.PageSize > 0 ? request.PageSize : 20,
CurrentPage = request.PageNumber > 0 ? request.PageNumber : 1,
TotalPage = 0
}
};
}
var query = new GetAllDiscountOrdersQuery
{
PaginationQuery = new Application.Common.Models.PaginationState
{
PageNumber = request.PageNumber > 0 ? request.PageNumber : 1,
PageSize = request.PageSize > 0 ? request.PageSize : 20
},
UserId = request.UserId,
UserMobile = request.UserMobile,
TrackingCode = request.TrackingCode,
FromDate = request.FromDate?.ToDateTime(),
ToDate = request.ToDate?.ToDateTime(),
MinAmount = request.MinAmount,
MaxAmount = request.MaxAmount
};
if (request.PaymentStatus is int paymentStatus)
query.PaymentStatus = MapProtoPaymentFilterToDomain(paymentStatus);
if (request.DeliveryStatus is int deliveryStatus)
query.DeliveryStatus = MapProtoDeliveryFilterToDomain(deliveryStatus);
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetAllDiscountOrdersResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
TotalCount = result.MetaData.TotalCount,
PageSize = result.MetaData.PageSize,
CurrentPage = result.MetaData.CurrentPage,
TotalPage = result.MetaData.TotalPage,
HasPrevious = result.MetaData.HasPrevious,
HasNext = result.MetaData.HasNext
}
};
foreach (var o in result.Models)
{
var model = new CMSMicroservice.Protobuf.Protos.DiscountOrder.AdminOrderDto
{
Id = o.Id,
UserId = o.UserId,
UserFullName = o.UserFullName ?? string.Empty,
UserMobile = o.UserMobile ?? string.Empty,
TotalAmount = o.TotalAmount,
DiscountBalanceUsed = o.DiscountBalanceUsed,
GatewayAmountPaid = o.GatewayAmountPaid,
VatAmount = o.VatAmount,
PaymentStatus = MapPaymentStatus(o.PaymentStatus),
DeliveryStatus = MapDeliveryStatus(o.DeliveryStatus),
ItemsCount = o.ItemsCount,
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(o.Created, DateTimeKind.Utc))
};
if (o.PaymentDate.HasValue)
model.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(o.PaymentDate.Value, DateTimeKind.Utc));
if (o.LastModified.HasValue)
model.LastModified = Timestamp.FromDateTime(DateTime.SpecifyKind(o.LastModified.Value, DateTimeKind.Utc));
if (!string.IsNullOrWhiteSpace(o.ShippingAddress))
model.ShippingAddress = o.ShippingAddress;
if (!string.IsNullOrWhiteSpace(o.ReceiverName))
model.ReceiverName = o.ReceiverName;
if (!string.IsNullOrWhiteSpace(o.ReceiverMobile))
model.ReceiverMobile = o.ReceiverMobile;
if (!string.IsNullOrWhiteSpace(o.TrackingCode))
model.TrackingCode = o.TrackingCode;
if (!string.IsNullOrWhiteSpace(o.AdminNote))
model.AdminNote = o.AdminNote;
response.Models.Add(model);
}
return response;
return await _dispatchRequestToCQRS.Handle<GetAllDiscountOrdersRequest, GetAllDiscountOrdersQuery, GetAllDiscountOrdersResponse>(request, context);
}
public override async Task<GetDiscountSalesReportResponse> GetDiscountSalesReport(GetDiscountSalesReportRequest request, ServerCallContext context)
@@ -463,50 +359,14 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB
_ => CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus.PaymentPending
};
/// <summary>
/// فیلتر UI/proto → دامنه: PENDING=0, COMPLETED=1, FAILED=2
/// </summary>
private static DomainEnums.PaymentStatus MapProtoPaymentFilterToDomain(int protoPaymentStatus) => protoPaymentStatus switch
{
1 => DomainEnums.PaymentStatus.Success, // PAYMENT_COMPLETED
2 => DomainEnums.PaymentStatus.Reject, // PAYMENT_FAILED
_ => DomainEnums.PaymentStatus.Pending // PAYMENT_PENDING (0) و پیش‌فرض
};
private static DeliveryStatus MapDeliveryStatus(DomainEnums.DeliveryStatus status) => status switch
{
DomainEnums.DeliveryStatus.None => DeliveryStatus.DeliveryPending,
DomainEnums.DeliveryStatus.Pending => DeliveryStatus.DeliveryProcessing,
DomainEnums.DeliveryStatus.InTransit => DeliveryStatus.DeliveryShipped,
DomainEnums.DeliveryStatus.Delivered => DeliveryStatus.DeliveryDelivered,
DomainEnums.DeliveryStatus.Returned => DeliveryStatus.DeliveryCancelled,
DomainEnums.DeliveryStatus.Cancelled => DeliveryStatus.DeliveryCancelled,
DomainEnums.DeliveryStatus.ReadyForOfficePickup => DeliveryStatus.DeliveryReadyForOffice,
DomainEnums.DeliveryStatus.Returned => DeliveryStatus.DeliveryReturned,
_ => DeliveryStatus.DeliveryPending
};
/// <summary>
/// فیلتر UI/proto → دامنه DeliveryStatus
/// </summary>
private static DomainEnums.DeliveryStatus MapProtoDeliveryFilterToDomain(int protoDeliveryStatus) => protoDeliveryStatus switch
{
1 => DomainEnums.DeliveryStatus.Pending, // DELIVERY_PROCESSING
2 => DomainEnums.DeliveryStatus.InTransit, // DELIVERY_SHIPPED
3 => DomainEnums.DeliveryStatus.Delivered, // DELIVERY_DELIVERED
4 => DomainEnums.DeliveryStatus.Cancelled, // DELIVERY_CANCELLED
5 => DomainEnums.DeliveryStatus.ReadyForOfficePickup, // DELIVERY_READY_FOR_OFFICE
6 => DomainEnums.DeliveryStatus.Returned, // DELIVERY_RETURNED
_ => DomainEnums.DeliveryStatus.None // DELIVERY_PENDING (0)
};
private static DomainEnums.DeliveryStatus MapProtoDeliveryToDomain(DeliveryStatus status) => status switch
{
DeliveryStatus.DeliveryProcessing => DomainEnums.DeliveryStatus.Pending,
DeliveryStatus.DeliveryShipped => DomainEnums.DeliveryStatus.InTransit,
DeliveryStatus.DeliveryDelivered => DomainEnums.DeliveryStatus.Delivered,
DeliveryStatus.DeliveryCancelled => DomainEnums.DeliveryStatus.Cancelled,
DeliveryStatus.DeliveryReadyForOffice => DomainEnums.DeliveryStatus.ReadyForOfficePickup,
DeliveryStatus.DeliveryReturned => DomainEnums.DeliveryStatus.Returned,
_ => DomainEnums.DeliveryStatus.None
};
}
@@ -185,11 +185,6 @@ public class NetworkMembershipService : NetworkMembershipContract.NetworkMembers
node.PackageId = dto.PackageId.Value;
}
node.GoldLeftLegTotal = dto.GoldLeftLegTotal;
node.GoldRightLegTotal = dto.GoldRightLegTotal;
node.SilverLeftLegTotal = dto.SilverLeftLegTotal;
node.SilverRightLegTotal = dto.SilverRightLegTotal;
if (dto.IsNewActivation.HasValue)
{
node.IsNewActivation = dto.IsNewActivation.Value;
@@ -32,7 +32,7 @@ public class PackageService : PackageContract.PackageContractBase
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
private readonly IPaymentGatewayResolver _paymentGatewayResolver;
private readonly IPaymentGatewayService _paymentGateway;
private readonly IConfiguration _configuration;
private readonly IUserPaymentLock _paymentLock;
@@ -41,7 +41,7 @@ public class PackageService : PackageContract.PackageContractBase
ISender sender,
IApplicationDbContext context,
ICurrentUserService currentUserService,
IPaymentGatewayResolver paymentGatewayResolver,
IPaymentGatewayService paymentGateway,
IConfiguration configuration,
IUserPaymentLock paymentLock)
{
@@ -49,7 +49,7 @@ public class PackageService : PackageContract.PackageContractBase
_sender = sender;
_context = context;
_currentUserService = currentUserService;
_paymentGatewayResolver = paymentGatewayResolver;
_paymentGateway = paymentGateway;
_configuration = configuration;
_paymentLock = paymentLock;
}
@@ -171,38 +171,19 @@ public class PackageService : PackageContract.PackageContractBase
if (package == null)
throw new RpcException(new Status(StatusCode.NotFound, "پکیج مورد نظر یافت نشد"));
var gatewayProvider = NormalizeGatewayProvider(request.GatewayProvider);
var isTwentyCard = gatewayProvider == "20card";
long gatewayAmount = package.Price;
if (isTwentyCard)
{
var packageKey = $"TwentyCard:Packages:{package.Id}";
gatewayAmount = _configuration.GetValue<long?>($"{packageKey}:GatewayAmountToman") ?? 0;
var walletCredit = _configuration.GetValue<long?>($"{packageKey}:WalletCreditToman") ?? 0;
if (gatewayAmount <= 0 || walletCredit <= 0)
{
throw new RpcException(new Status(
StatusCode.FailedPrecondition,
"پرداخت ۲۰کارت برای این پکیج پیکربندی نشده است"));
}
}
// Create transaction — 20card: Amount = gateway charge; zarinpal: Package.Price
// Create transaction
var transaction = new CMSMicroservice.Domain.Entities.Transaction
{
Amount = gatewayAmount,
Description = isTwentyCard
? $"خرید پکیج {package.Title} (۲۰کارت)"
: $"خرید پکیج {package.Title}",
Amount = package.Price,
Description = $"خرید پکیج {package.Title}",
PaymentStatus = Domain.Enums.PaymentStatus.Pending,
Type = Domain.Enums.TransactionType.Buy
};
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(cancellationToken);
// Create purchase record — UPP.Amount always Package.Price (catalog)
// Create purchase record
var purchaseMethod = request.PurchaseMethod == PurchaseMethodEnum.PurchaseMethodGateway
? Domain.Enums.PackagePurchaseMethod.DirectPurchase
: Domain.Enums.PackagePurchaseMethod.DayaLoan;
@@ -223,32 +204,20 @@ public class PackageService : PackageContract.PackageContractBase
var user = await _context.Users
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u => new { u.Mobile, u.FirstName, u.LastName, u.Email, u.NationalCode })
.Select(u => new { u.Mobile })
.FirstOrDefaultAsync(cancellationToken);
// Callback URL از config — نه از ورودی کاربر (امنیت)
// ۲۰کارت: CallbackBaseUrl اختیاری (مثلاً استیج → kbs2.ir چون دامنه استیج در ترمینال ثبت نیست)
var frontOfficeBaseUrl = isTwentyCard
? (_configuration["TwentyCard:CallbackBaseUrl"]
?? _configuration["FrontOfficeBaseUrl"]
?? "https://localhost:5268")
: (_configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268");
var callbackUrl = $"{frontOfficeBaseUrl.TrimEnd('/')}/profile/payment-callback?orderId={purchase.Id}";
var paymentGateway = _paymentGatewayResolver.Get(gatewayProvider);
var payerName = $"{user?.FirstName} {user?.LastName}".Trim();
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
var callbackUrl = $"{frontOfficeBaseUrl}/profile/payment-callback?orderId={purchase.Id}";
var paymentResult = await paymentGateway.InitiatePaymentAsync(new PaymentRequest
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
Amount = gatewayAmount,
Amount = package.Price,
UserId = userId,
Mobile = user?.Mobile ?? string.Empty,
Name = payerName,
Invoice = purchase.Id.ToString(),
Description = $"خرید پکیج {package.Title}",
CallbackUrl = callbackUrl,
Email = user?.Email,
NationalId = user?.NationalCode
CallbackUrl = callbackUrl
}, cancellationToken);
if (!paymentResult.IsSuccess)
@@ -266,14 +235,12 @@ public class PackageService : PackageContract.PackageContractBase
// Save RefId
transaction.RefId = paymentResult.RefId;
// ثبت PaymentTransaction — Amount = gateway amount
// ثبت PaymentTransaction
var paymentTx = new PaymentTransaction
{
GatewayProvider = gatewayProvider,
MerchantId = isTwentyCard
? (_configuration["TwentyCard:TerminalId"] ?? "")
: (_configuration["ZarinPal:MerchantId"] ?? ""),
Amount = gatewayAmount,
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
Amount = package.Price,
CallbackUrl = callbackUrl,
Description = $"خرید پکیج {package.Title}",
Mobile = user?.Mobile,
@@ -297,14 +264,6 @@ public class PackageService : PackageContract.PackageContractBase
Authority = paymentResult.RefId ?? string.Empty
};
}
private static string NormalizeGatewayProvider(string? provider)
{
if (string.IsNullOrWhiteSpace(provider))
return "zarinpal";
var key = provider.Trim().ToLowerInvariant();
return key is "20card" or "twentycard" ? "20card" : "zarinpal";
}
public override async Task<CustomerVerifyPackagePurchaseResponse> CustomerVerifyPackagePurchase(
CustomerVerifyPackagePurchaseRequest request,
@@ -121,9 +121,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
TrackingCode = result.TrackingCode,
DeliveryDescription = result.DeliveryDescription,
UserFullName = result.UserFullName,
UserNationalCode = result.UserNationalCode,
PostalCode = result.PostalCode,
UserMobile = result.UserMobile
UserNationalCode = result.UserNationalCode
};
// VAT Info
@@ -791,9 +789,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
TrackingCode = result.TrackingCode,
DeliveryDescription = result.DeliveryDescription,
UserFullName = result.UserFullName,
UserNationalCode = result.UserNationalCode,
PostalCode = result.PostalCode,
UserMobile = result.UserMobile
UserNationalCode = result.UserNationalCode
};
// VAT Info
@@ -1058,7 +1054,6 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
Domain.Enums.DeliveryStatus.Delivered => "تحویل داده شده",
Domain.Enums.DeliveryStatus.Returned => "مرجوع شده",
Domain.Enums.DeliveryStatus.Cancelled => "لغو شده",
Domain.Enums.DeliveryStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
_ => status.ToString()
};
}
@@ -2,7 +2,6 @@ using CMSMicroservice.Protobuf.Protos.UserPackagePurchase;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetAllUserPackagePurchaseByFilter;
using CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetUserPackagePurchaseSummary;
using CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetCustomerPackagePurchaseRollup;
namespace CMSMicroservice.WebApi.Services;
@@ -19,8 +18,4 @@ public class UserPackagePurchaseService : UserPackagePurchaseContract.UserPackag
public override Task<GetUserPackagePurchaseSummaryResponse> GetUserPackagePurchaseSummary(
GetUserPackagePurchaseSummaryRequest request, ServerCallContext context) =>
_dispatch.Handle<GetUserPackagePurchaseSummaryRequest, GetUserPackagePurchaseSummaryQuery, GetUserPackagePurchaseSummaryResponse>(request, context);
public override Task<GetCustomerPackagePurchaseRollupResponse> GetCustomerPackagePurchaseRollup(
GetCustomerPackagePurchaseRollupRequest request, ServerCallContext context) =>
_dispatch.Handle<GetCustomerPackagePurchaseRollupRequest, GetCustomerPackagePurchaseRollupQuery, GetCustomerPackagePurchaseRollupResponse>(request, context);
}
@@ -15,11 +15,8 @@ using CMSMicroservice.Application.UserCQ.Commands.VerifyOtpToken;
using CMSMicroservice.Application.UserCQ.Commands.AcceptContract;
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerProfile;
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerReferrals;
using CMSMicroservice.Application.UserCQ.Queries.GetMyInvitees;
using CMSMicroservice.Application.UserCQ.Queries.GetInvitationsByFilter;
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerSettings;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Authorization;
using Google.Protobuf.WellKnownTypes;
using Microsoft.EntityFrameworkCore;
using Grpc.Core;
@@ -315,117 +312,6 @@ public class UserService : UserContract.UserContractBase
return response;
}
public override async Task<GetMyInviteesResponse> GetMyInvitees(
GetMyInviteesRequest request,
ServerCallContext context)
{
var result = await _sender.Send(new GetMyInviteesQuery
{
UserId = 0,
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
ClubStatusFilter = request.ClubStatusFilter
}, context.CancellationToken);
var response = new GetMyInviteesResponse
{
MetaData = new 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
},
Stats = new InviteeStats
{
TotalInvitees = result.Stats.TotalInvitees,
ClubActiveCount = result.Stats.ClubActiveCount,
ClubInactiveCount = result.Stats.ClubInactiveCount
}
};
foreach (var item in result.Invitees)
{
response.Invitees.Add(new InviteeModel
{
UserId = item.UserId,
FirstName = item.FirstName,
LastName = item.LastName,
Mobile = item.Mobile,
RegisteredAt = Timestamp.FromDateTime(DateTime.SpecifyKind(item.RegisteredAt, DateTimeKind.Utc)),
IsClubActive = item.IsClubActive
});
}
return response;
}
[RequiresPermission(PermissionNames.UsersView)]
public override async Task<GetInvitationsByFilterResponse> GetInvitationsByFilter(
GetInvitationsByFilterRequest request,
ServerCallContext context)
{
DateTime? createdFrom = null;
DateTime? createdTo = null;
if (request.Filter?.CreatedFrom != null)
createdFrom = request.Filter.CreatedFrom.ToDateTime();
if (request.Filter?.CreatedTo != null)
createdTo = request.Filter.CreatedTo.ToDateTime();
var result = await _sender.Send(new GetInvitationsByFilterQuery
{
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
SortBy = request.SortBy,
Filter = request.Filter == null ? null : new GetInvitationsByFilterFilterDto
{
InviterMobile = request.Filter.InviterMobile,
InviterFirstName = request.Filter.InviterFirstName,
InviterLastName = request.Filter.InviterLastName,
InviteeMobile = request.Filter.InviteeMobile,
InviteeFirstName = request.Filter.InviteeFirstName,
InviteeLastName = request.Filter.InviteeLastName,
InviteeClubActive = request.Filter.InviteeClubActive,
CreatedFrom = createdFrom,
CreatedTo = createdTo
}
}, context.CancellationToken);
var response = new GetInvitationsByFilterResponse
{
MetaData = new 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)
{
response.Models.Add(new InvitationReportModel
{
Id = m.Id,
InviterUserId = m.InviterUserId,
InviterFirstName = m.InviterFirstName,
InviterLastName = m.InviterLastName,
InviterMobile = m.InviterMobile,
InviteeUserId = m.InviteeUserId,
InviteeFirstName = m.InviteeFirstName,
InviteeLastName = m.InviteeLastName,
InviteeMobile = m.InviteeMobile,
InviteeRegisteredAt = Timestamp.FromDateTime(DateTime.SpecifyKind(m.InviteeRegisteredAt, DateTimeKind.Utc)),
InviteeClubActive = m.InviteeClubActive,
InvitedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(m.InvitedAt, DateTimeKind.Utc))
});
}
return response;
}
public override async Task<UploadCustomerAvatarResponse> UploadCustomerAvatar(UploadCustomerAvatarRequest request, ServerCallContext context)
{
if (request.FileData == null || request.FileData.Length == 0)
@@ -18,7 +18,6 @@ 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;
@@ -176,18 +175,7 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
// Update payout with withdrawal info
payout.WithdrawalMethod = (Domain.Enums.WithdrawalMethod)request.WithdrawalMethod;
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.IbanNumber = request.IbanNumber;
payout.Status = Domain.Enums.CommissionPayoutStatus.WithdrawRequested;
await _context.SaveChangesAsync(context.CancellationToken);
@@ -2,38 +2,16 @@
"PaymentProvider": "zarinpal",
"ZarinPal": {
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
"UseSandbox": true
},
"TwentyCard": {
"BaseUrl": "https://gateway.20card.ir/ipg",
"TerminalId": "165674",
"TerminalUsername": "ea5y1biehi",
"TerminalPassword": "SaLamAT@1404!",
"CreditPortalUrl": "https://userpanel.20card.ir/",
"CallbackBaseUrl": "https://kbs2.ir",
"DefaultEmail": "info@kbs2.ir",
"Packages": {
"4": {
"GatewayAmountToman": 11200000,
"WalletCreditToman": 12656000
},
"1": {
"GatewayAmountToman": 11200000,
"WalletCreditToman": 12656000
}
}
},
"CmsBaseUrl": "https://cms.se.kbs1.ir",
"FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir",
"FMS": {
"Address": "https://dl.afrino.co"
"UseSandbox": false
},
"CmsBaseUrl": "https://cms.kbs2.ir",
"FrontOfficeBaseUrl": "https://kbs2.ir",
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
"JwtIssuer": "https://localhost",
"JwtAudience": "https://localhost",
"JwtExpiryInDays": 5,
"ConnectionStrings": {
"DefaultConnection": "Data Source=185.252.31.44,31433; Initial Catalog=Foursat;User ID=sa;Password=87zH26nbqT;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
"DefaultConnection": "Server=mssql-svc;Database=KBS;User Id=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=True;",
"providerName": "System.Data.SqlClient"
},
"Otp": {
@@ -42,12 +20,12 @@
},
"Monitoring": {
"SentryEnabled": false,
"SentryDsn": "",
"SentryDsn": "",
"SlackEnabled": false,
"SlackWebhookUrl": "",
"EmailAlertsEnabled": false,
"AdminEmails": [
"admin@example.com"
"admin@example.com"
],
"SmsNotificationsEnabled": false,
"SmsApiKey": "",
@@ -86,7 +64,7 @@
},
"BackgroundJobs": {
"WeeklyCommissionCalculation": {
"Enabled": true,
"Enabled": false,
"CronExpression": "5 0 * * 0"
}
},
@@ -106,7 +84,13 @@
"Audience": "domain_api"
},
"Seq": {
"ServerUrl": "https://seq.afrino.co",
"ServerUrl": "http://seq-svc:5341",
"ApiKey": "oxpvpUzU1pZxMS4s3Fqq"
},
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
}
}
}
+20 -35
View File
@@ -2,42 +2,16 @@
"PaymentProvider": "zarinpal",
"ZarinPal": {
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
"UseSandbox": true
},
"TwentyCard": {
"BaseUrl": "https://gateway.20card.ir/ipg",
"TerminalId": "165674",
"TerminalUsername": "ea5y1biehi",
"TerminalPassword": "SaLamAT@1404!",
"CreditPortalUrl": "https://userpanel.20card.ir/",
"DefaultEmail": "info@kbs2.ir",
"Packages": {
"4": {
"GatewayAmountToman": 11200000,
"WalletCreditToman": 12656000
},
"1": {
"GatewayAmountToman": 11200000,
"WalletCreditToman": 12656000
}
}
},
"CmsBaseUrl": "https://localhost:32846",
"FrontOfficeBaseUrl": "http://localhost:5268",
"FMS": {
"Address": "https://dl.afrino.co"
"UseSandbox": false
},
"CmsBaseUrl": "https://cms.kbs2.ir",
"FrontOfficeBaseUrl": "https://kbs2.ir",
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
"JwtIssuer": "https://localhost",
"JwtAudience": "https://localhost",
"JwtExpiryInDays": 5,
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http1AndHttp2"
}
},
"ConnectionStrings": {
"DefaultConnection": "Data Source=185.252.31.44,31433; Initial Catalog=Foursat;User ID=sa;Password=87zH26nbqT;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
"DefaultConnection": "Server=45.149.79.127,31433;Database=KBS;User Id=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=True;",
"providerName": "System.Data.SqlClient"
},
"Otp": {
@@ -45,12 +19,12 @@
},
"Monitoring": {
"SentryEnabled": false,
"SentryDsn": "",
"SentryDsn": "",
"SlackEnabled": false,
"SlackWebhookUrl": "",
"EmailAlertsEnabled": false,
"AdminEmails": [
"admin@example.com"
"admin@example.com"
],
"SmsNotificationsEnabled": false,
"SmsApiKey": "",
@@ -69,7 +43,7 @@
"Sms": {
"Enabled": true,
"Provider": "Kavenegar",
"KavenegarApiKey": "43676A4E786A6C50452F2F6252507939346A5145764B33566B456454374E657A614468706658656D6534593D",
"KavenegarApiKey": "497263626F32626A48685A6137524C4F78575A766E4C74694A556B79317648424964655030682B554545413D",
"Sender": "1000001110100"
},
"DayaPayment": {
@@ -89,7 +63,7 @@
},
"BackgroundJobs": {
"WeeklyCommissionCalculation": {
"Enabled": true,
"Enabled": false,
"CronExpression": "5 0 * * 0"
}
},
@@ -102,12 +76,23 @@
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
},
"Authentication": {
"Authority": "https://ids.domain.com/",
"Audience": "domain_api"
},
"Seq": {
"ServerUrl": "https://seq.afrino.co",
"ServerUrl": "http://seq-svc:5341",
"ApiKey": "oxpvpUzU1pZxMS4s3Fqq"
},
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
}
}
}
+2 -2
View File
@@ -3,8 +3,8 @@
<packageSources>
<clear />
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
<add key="Nexus" value="http://185.252.31.44:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
<add key="foursat-hosted" value="http://185.252.31.44:32081/repository/foursat-nuget-hosted/index.json" allowInsecureConnections="true" />
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
<add key="foursat-hosted" value="http://194.5.195.53:32081/repository/foursat-nuget-hosted/index.json" allowInsecureConnections="true" />
</packageSources>
<packageSourceCredentials>
<Nexus>
@@ -1,257 +0,0 @@
/*
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.';
@@ -1,246 +0,0 @@
/*
DIAGNOSTIC ONLY — maps RefId LIKE 'manualy%' transactions to Users / WalletHistory.
Background (ChargeUserWallets.sql):
RefId = 'manualy' + ChangeLogId at charge time (original UserWalletChangeLogs id).
After migration/rebuild, WH ids shifted; RefrenceId on some rows stayed 0.
When RefrenceId=0, CreatedBy may store the Transaction Id as a fallback hint —
but Tx may never have been created; do NOT infer Tx from mobile or manualy suffix alone.
Rules used here:
- WH.RefrenceId > 0 → trusted link to Tx
- WH.RefrenceId = 0 → Tx ONLY if CreatedBy is numeric AND that Tx exists with RefId manualy%
- Tx → User via mobile (CreatedBy/LastModifiedBy) is informational, not used alone for Ref=0 WH
- manualy{N} suffix N = original ChangeLog id (may differ from current WH.Id)
Run on staging before script 6.
*/
SET NOCOUNT ON;
DECLARE @ManualyTx TABLE (
TransactionId BIGINT PRIMARY KEY,
RefId NVARCHAR(100) NOT NULL,
OriginalChangeLogId BIGINT NULL,
Amount BIGINT,
TxCreatedBy NVARCHAR(200),
TxLastModifiedBy NVARCHAR(200),
PaymentDate DATETIME2
);
INSERT INTO @ManualyTx (TransactionId, RefId, OriginalChangeLogId, Amount, TxCreatedBy, TxLastModifiedBy, PaymentDate)
SELECT
t.Id,
t.RefId,
TRY_CAST(
SUBSTRING(t.RefId, LEN(N'manualy') + 1, 20)
AS BIGINT
),
t.Amount,
t.CreatedBy,
t.LastModifiedBy,
t.PaymentDate
FROM CMS.Transactions t
WHERE t.IsDeleted = 0
AND t.RefId LIKE N'manualy%';
DECLARE @ManualyTxCount INT = (SELECT COUNT(*) FROM @ManualyTx);
PRINT CONCAT('manualy transactions in DB: ', @ManualyTxCount);
------------------------------------------------------------
-- 1) Each manualy Tx → User(s) via WH.RefrenceId / CreatedBy hint / mobile
------------------------------------------------------------
PRINT '=== 1) manualy Tx → User resolution ===';
SELECT
mt.TransactionId,
mt.RefId,
mt.OriginalChangeLogId,
mt.Amount,
mt.PaymentDate,
mt.TxCreatedBy,
mt.TxLastModifiedBy,
u_mobile.Id AS UserByMobile,
u_mobile.Mobile AS UserByMobilePhone,
u_mobile.FirstName AS UserByMobileFirst,
u_mobile.LastName AS UserByMobileLast,
h_linked.Id AS WhIdViaRef,
h_linked.WalletId AS WhWalletViaRef,
u_wh.Id AS UserViaWhRef,
h_hint.Id AS WhIdViaCreatedByHint,
h_hint.WalletId AS WhWalletViaHint,
u_hint.Id AS UserViaWhHint,
CASE
WHEN u_wh.Id IS NOT NULL THEN N'WH_REFRENCE_ID'
WHEN u_hint.Id IS NOT NULL THEN N'WH_CREATEDBY_HINT'
WHEN u_mobile.Id IS NOT NULL THEN N'TX_MOBILE_ONLY'
ELSE N'UNRESOLVED'
END AS PrimaryLink,
CASE
WHEN u_wh.Id IS NOT NULL AND u_hint.Id IS NOT NULL AND u_wh.Id <> u_hint.Id
THEN N'CONFLICT_WH_REF_VS_HINT'
WHEN u_wh.Id IS NOT NULL AND u_mobile.Id IS NOT NULL AND u_wh.Id <> u_mobile.Id
THEN N'CONFLICT_WH_REF_VS_MOBILE'
WHEN u_wh.Id IS NULL AND u_hint.Id IS NULL AND u_mobile.Id IS NULL
THEN N'NO_USER_FOUND'
ELSE N'OK'
END AS LinkQuality
FROM @ManualyTx mt
LEFT JOIN CMS.Users u_mobile ON u_mobile.IsDeleted = 0
AND u_mobile.Mobile IN (
NULLIF(LTRIM(RTRIM(mt.TxCreatedBy)), N''),
NULLIF(LTRIM(RTRIM(mt.TxLastModifiedBy)), N'')
)
LEFT JOIN CMS.UserWalletHistories h_linked ON h_linked.IsDeleted = 0
AND h_linked.RefrenceId = mt.TransactionId
LEFT JOIN CMS.UserWallets w_linked ON w_linked.Id = h_linked.WalletId AND w_linked.IsDeleted = 0
LEFT JOIN CMS.Users u_wh ON u_wh.Id = w_linked.UserId AND u_wh.IsDeleted = 0
LEFT JOIN CMS.UserWalletHistories h_hint ON h_hint.IsDeleted = 0
AND (h_hint.RefrenceId IS NULL OR h_hint.RefrenceId = 0)
AND TRY_CAST(h_hint.CreatedBy AS BIGINT) = mt.TransactionId
LEFT JOIN CMS.UserWallets w_hint ON w_hint.Id = h_hint.WalletId AND w_hint.IsDeleted = 0
LEFT JOIN CMS.Users u_hint ON u_hint.Id = w_hint.UserId AND u_hint.IsDeleted = 0
ORDER BY mt.TransactionId;
------------------------------------------------------------
-- 2) WH rows with RefrenceId=0 — CreatedBy hint analysis
------------------------------------------------------------
PRINT '=== 2) WalletHistory RefrenceId=0 (manualy charge candidates) ===';
SELECT
h.Id AS WalletHistoryId,
h.WalletId,
w.UserId AS WalletUserId,
u.Mobile AS WalletUserMobile,
h.RefrenceId AS WhRefrenceId,
h.ChangeValue,
h.Created AS WhCreated,
h.CreatedBy AS WhCreatedBy,
h.LastModifiedBy AS WhLastModifiedBy,
t_hint.Id AS TxByCreatedByHint,
t_hint.RefId AS TxHintRefId,
t_hint.Amount AS TxHintAmount,
u_hint_mobile.Id AS TxHintUserByMobile,
CASE
WHEN TRY_CAST(h.CreatedBy AS BIGINT) IS NULL OR TRY_CAST(h.CreatedBy AS BIGINT) = 0
THEN N'NO_NUMERIC_HINT'
WHEN t_hint.Id IS NULL
THEN N'HINT_POINTS_TO_MISSING_TX'
WHEN t_hint.RefId NOT LIKE N'manualy%'
THEN N'HINT_TX_NOT_MANUALY'
WHEN w.UserId = u_hint_mobile.Id
THEN N'FIX_LINK_WH_TO_HINT_TX'
WHEN u_hint_mobile.Id IS NOT NULL AND w.UserId <> u_hint_mobile.Id
THEN N'HINT_TX_USER_MISMATCH_WALLET'
ELSE N'FIX_LINK_REVIEW_MOBILE'
END AS WhZeroRefVerdict,
CASE
WHEN t_hint.Id IS NOT NULL AND t_hint.RefId LIKE N'manualy%'
THEN CONCAT(N'UPDATE WH ', h.Id, N' SET RefrenceId=', t_hint.Id)
WHEN TRY_CAST(h.CreatedBy AS BIGINT) IS NULL OR TRY_CAST(h.CreatedBy AS BIGINT) = 0
THEN N'Cannot auto-link — no CreatedBy Tx hint'
ELSE N'Tx from hint missing — may need manual charge review'
END AS SuggestedAction
FROM CMS.UserWalletHistories h
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
LEFT JOIN CMS.Users u ON u.Id = w.UserId AND u.IsDeleted = 0
LEFT JOIN CMS.Transactions t_hint ON t_hint.Id = TRY_CAST(h.CreatedBy AS BIGINT)
AND t_hint.IsDeleted = 0
LEFT JOIN CMS.Users u_hint_mobile ON u_hint_mobile.IsDeleted = 0
AND u_hint_mobile.Mobile IN (
NULLIF(LTRIM(RTRIM(t_hint.CreatedBy)), N''),
NULLIF(LTRIM(RTRIM(t_hint.LastModifiedBy)), N'')
)
WHERE h.IsDeleted = 0
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
AND (
TRY_CAST(h.CreatedBy AS BIGINT) IS NOT NULL
OR h.LastModifiedBy LIKE N'09%'
OR EXISTS (
SELECT 1 FROM @ManualyTx mt
WHERE mt.OriginalChangeLogId = h.Id
)
)
ORDER BY h.Id;
------------------------------------------------------------
-- 3) manualy suffix vs current WH.Id (id-shift detector)
------------------------------------------------------------
PRINT '=== 3) manualy suffix N vs current WH.Id (id shift) ===';
SELECT
mt.TransactionId,
mt.RefId,
mt.OriginalChangeLogId AS ManualySuffixN,
h_by_suffix.Id AS CurrentWhIdWithSameIdAsSuffix,
h_by_suffix.RefrenceId AS ThatWhRefrenceId,
h_by_suffix.WalletId,
CASE
WHEN h_by_suffix.Id IS NULL THEN N'SUFFIX_WH_ID_GONE'
WHEN h_by_suffix.RefrenceId = mt.TransactionId THEN N'SUFFIX_WH_LINKED_OK'
WHEN h_by_suffix.RefrenceId IS NULL OR h_by_suffix.RefrenceId = 0
THEN N'SUFFIX_WH_EXISTS_BROKEN_REF'
ELSE N'SUFFIX_WH_POINTS_OTHER_TX'
END AS SuffixMatchStatus
FROM @ManualyTx mt
LEFT JOIN CMS.UserWalletHistories h_by_suffix ON h_by_suffix.Id = mt.OriginalChangeLogId
AND h_by_suffix.IsDeleted = 0
ORDER BY mt.TransactionId;
------------------------------------------------------------
-- 4) Distinct users in scope for script 6 (manualy-linked only)
------------------------------------------------------------
PRINT '=== 4) Users in manualy fix scope ===';
;WITH ScopeUsers AS (
SELECT DISTINCT w.UserId, mt.TransactionId, N'WH_REF' AS Source
FROM @ManualyTx mt
INNER JOIN CMS.UserWalletHistories h ON h.RefrenceId = mt.TransactionId AND h.IsDeleted = 0
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
UNION
SELECT DISTINCT w.UserId, t.Id, N'WH_CREATEDBY_HINT'
FROM CMS.UserWalletHistories h
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
WHERE h.IsDeleted = 0
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0
UNION
SELECT DISTINCT u.Id, mt.TransactionId, N'TX_MOBILE'
FROM @ManualyTx mt
INNER JOIN CMS.Users u ON u.IsDeleted = 0
AND u.Mobile IN (
NULLIF(LTRIM(RTRIM(mt.TxCreatedBy)), N''),
NULLIF(LTRIM(RTRIM(mt.TxLastModifiedBy)), N'')
)
)
SELECT
su.UserId,
u.Mobile,
u.FirstName,
u.LastName,
u.PackagePurchaseMethod,
MIN(su.TransactionId) AS SampleManualyTxId,
STRING_AGG(su.Source, N', ') WITHIN GROUP (ORDER BY su.Source) AS LinkSources,
CASE WHEN EXISTS (
SELECT 1 FROM CMS.Transactions t2
WHERE t2.IsDeleted = 0 AND t2.RefId LIKE N'C4%'
AND EXISTS (
SELECT 1 FROM CMS.Users ux
WHERE ux.Id = su.UserId AND ux.IsDeleted = 0
AND ux.Mobile IN (NULLIF(t2.CreatedBy,N''), NULLIF(t2.LastModifiedBy,N''))
)
) THEN 1 ELSE 0 END AS AlsoHasC4Tx,
(SELECT COUNT(*) FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = su.UserId AND upp.IsDeleted = 0) AS UppCount
FROM ScopeUsers su
INNER JOIN CMS.Users u ON u.Id = su.UserId AND u.IsDeleted = 0
GROUP BY su.UserId, u.Mobile, u.FirstName, u.LastName, u.PackagePurchaseMethod
ORDER BY su.UserId;
PRINT 'Done (diagnostic only — no changes).';
-104
View File
@@ -1,104 +0,0 @@
"TransactionId","RefId","OriginalChangeLogId","Amount","PaymentDate","TxCreatedBy","TxLastModifiedBy","UserByMobile","UserByMobilePhone","UserByMobileFirst","UserByMobileLast","WhIdViaRef","WhWalletViaRef","UserViaWhRef","WhIdViaCreatedByHint","WhWalletViaHint","UserViaWhHint","PrimaryLink","LinkQuality"
"23","manualy24","24","56000000","19/11/2025 01:24:24",,,,,,,"102","38","75",,,,"WH_REFRENCE_ID","OK"
"23","manualy24","24","56000000","19/11/2025 01:24:24",,,,,,,"216","24","47",,,,"WH_REFRENCE_ID","OK"
"24","manualy25","25","56000000","19/11/2025 01:24:24",,,,,,,"103","46","93",,,,"WH_REFRENCE_ID","OK"
"24","manualy25","25","56000000","19/11/2025 01:24:24",,,,,,,"217","6","50",,,,"WH_REFRENCE_ID","OK"
"25","manualy26","26","56000000","19/11/2025 01:24:24",,,,,,,"104","47","96","218","34","71","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
"26","manualy27","27","56000000","19/11/2025 01:24:24",,,,,,,"105","49","99","219","37","74","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
"28","manualy29","29","56000000","19/11/2025 01:24:24",,,,,,,"106","24","47",,,,"WH_REFRENCE_ID","OK"
"28","manualy29","29","56000000","19/11/2025 01:24:24",,,,,,,"221","42","88",,,,"WH_REFRENCE_ID","OK"
"29","manualy30","30","56000000","19/11/2025 01:24:24",,,,,,,"107","22","43",,,,"WH_REFRENCE_ID","OK"
"29","manualy30","30","56000000","19/11/2025 01:24:24",,,,,,,"222","62","114",,,,"WH_REFRENCE_ID","OK"
"30","manualy31","31","56000000","19/11/2025 01:24:24",,,,,,,"108","23","45",,,,"WH_REFRENCE_ID","OK"
"30","manualy31","31","56000000","19/11/2025 01:24:24",,,,,,,"223","13","7",,,,"WH_REFRENCE_ID","OK"
"31","manualy32","32","56000000","28/11/2025 05:27:38",,,,,,,"122","26","52",,,,"WH_REFRENCE_ID","OK"
"31","manualy32","32","56000000","28/11/2025 05:27:38",,,,,,,"224","15","9",,,,"WH_REFRENCE_ID","OK"
"42","manualy43","43","56000000","3/12/2025 09:49:45.92","09014638673","09014638673","116","09014638673","سالار","رضائی","138","64","116",,,,"WH_REFRENCE_ID","OK"
"42","manualy43","43","56000000","3/12/2025 09:49:45.92","09014638673","09014638673","116","09014638673","سالار","رضائی","235","24","47",,,,"WH_REFRENCE_ID","CONFLICT_WH_REF_VS_MOBILE"
"43","manualy44","44","56000000","3/12/2025 09:49:45.92","09056365323","09056365323","115","09056365323","حامد","دهقانی","139","63","115","236","32","69","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
"44","manualy45","45","56000000","3/12/2025 09:49:45.92","09126092994","09126092994","119","09126092994","علیرضا ","کریمی پیروز ","140","67","119",,,,"WH_REFRENCE_ID","OK"
"44","manualy45","45","56000000","3/12/2025 09:49:45.92","09126092994","09126092994","119","09126092994","علیرضا ","کریمی پیروز ","237","38","75",,,,"WH_REFRENCE_ID","CONFLICT_WH_REF_VS_MOBILE"
"45","manualy46","46","56000000","3/12/2025 09:49:45.92","09177325630","09177325630","113","09177325630","کاوس","بیگ اینالو","141","61","113",,,,"WH_REFRENCE_ID","OK"
"45","manualy46","46","56000000","3/12/2025 09:49:45.92","09177325630","09177325630","113","09177325630","کاوس","بیگ اینالو","238","6","50",,,,"WH_REFRENCE_ID","CONFLICT_WH_REF_VS_MOBILE"
"46","manualy47","47","56000000","3/12/2025 09:49:45.92","09384366040","09384366040","103","09384366040","علی ","دلال زاده","142","51","103",,,,"WH_REFRENCE_ID","OK"
"46","manualy47","47","56000000","3/12/2025 09:49:45.92","09384366040","09384366040","103","09384366040","علی ","دلال زاده","239","13","7",,,,"WH_REFRENCE_ID","CONFLICT_WH_REF_VS_MOBILE"
"56","manualy57","57","56000000","3/12/2025 09:49:45.92","09015793338","09015793338","71","09015793338","حمیدرضا","مرجانی","249","62","114","118","34","71","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
"57","manualy58","58","56000000","19/11/2025 01:24:24","09961354257","09961354257",,,,,"250","13","7","124","35","72","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
"58","manualy59","59","56000000","19/11/2025 01:24:24","09217672935","09217672935","69","09217672935","مهدی ","گیوی ","251","15","9","119","32","69","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
"59","manualy60","60","56000000","19/11/2025 01:24:24","09101692049","09101692049","74","09101692049","سجاد ","اداوی","252","22","43","120","37","74","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
"68","manualy69","69","56000000","8/12/2025 12:30:53.536666","09399172812","09399172812","142","09399172812","صغری","شبانکاره ",,,,,,,"TX_MOBILE_ONLY","OK"
"107","manualy292","292","56000000","30/12/2025 01:24:24",,,,,,,"186","15","9",,,,"WH_REFRENCE_ID","OK"
"107","manualy292","292","56000000","30/12/2025 01:24:24",,,,,,,"292","118","170",,,,"WH_REFRENCE_ID","OK"
"107","manualy292","292","56000000","30/12/2025 01:24:24",,,,,,,"40399","34","71",,,,"WH_REFRENCE_ID","OK"
"108","manualy293","293","56000000","31/12/2025 01:24:24",,,,,,,"187","22","43",,,,"WH_REFRENCE_ID","OK"
"108","manualy293","293","56000000","31/12/2025 01:24:24",,,,,,,"293","119","171",,,,"WH_REFRENCE_ID","OK"
"108","manualy293","293","56000000","31/12/2025 01:24:24",,,,,,,"40400","38","75",,,,"WH_REFRENCE_ID","OK"
"109","manualy294","294","56000000","31/12/2025 01:24:24",,,,,,,"188","23","45",,,,"WH_REFRENCE_ID","OK"
"109","manualy294","294","56000000","31/12/2025 01:24:24",,,,,,,"294","120","172",,,,"WH_REFRENCE_ID","OK"
"109","manualy294","294","56000000","31/12/2025 01:24:24",,,,,,,"40401","59","111",,,,"WH_REFRENCE_ID","OK"
"110","manualy295","295","56000000","1/1/2026 17:07:53.2301978",,,,,,,"189","24","47",,,,"WH_REFRENCE_ID","OK"
"110","manualy295","295","56000000","1/1/2026 17:07:53.2301978",,,,,,,"295","123","175",,,,"WH_REFRENCE_ID","OK"
"110","manualy295","295","56000000","1/1/2026 17:07:53.2301978",,,,,,,"40402","134","186",,,,"WH_REFRENCE_ID","OK"
"118","34","71","09015793338","0","1","3/12/2025 09:49:45.92","56","09015793338","56","manualy57","56000000","71","FIX_LINK_WH_TO_HINT_TX","UPDATE WH 118 SET RefrenceId=56"
"119","32","69","09217672935","0","1","19/11/2025 01:24:24","58","09217672935","58","manualy59","56000000","69","FIX_LINK_WH_TO_HINT_TX","UPDATE WH 119 SET RefrenceId=58"
"120","37","74","09101692049","0","1","19/11/2025 01:24:24","59","09101692049","59","manualy60","56000000","74","FIX_LINK_WH_TO_HINT_TX","UPDATE WH 120 SET RefrenceId=59"
"121","12","2","09387342688","0","1","19/12/2025 22:00:40.276666","0","09387342688",,,,,"NO_NUMERIC_HINT","Cannot auto-link — no CreatedBy Tx hint"
"124","35","72","09124678697","0","1","19/11/2025 01:24:24","57","09961354257","57","manualy58","56000000",,"FIX_LINK_REVIEW_MOBILE","UPDATE WH 124 SET RefrenceId=57"
"201","110","162","09199877503","0","1","20/12/2025 02:24:42.771904","92","162",,,,,"HINT_POINTS_TO_MISSING_TX","Tx from hint missing — may need manual charge review"
"209","32","69","09217672935","0","0","20/12/2025 10:30:49.186666","16","SP","16","localwallet-d0870aee-8a21-4cfc-a5a0-362b25795af0","10000",,"HINT_TX_NOT_MANUALY","Tx from hint missing — may need manual charge review"
"218","34","71","09015793338","0","0","20/12/2025 10:31:12.52","25","SP","25","manualy26","56000000",,"FIX_LINK_REVIEW_MOBILE","UPDATE WH 218 SET RefrenceId=25"
"219","37","74","09101692049","0","0","20/12/2025 10:31:12.52","26","SP","26","manualy27","56000000",,"FIX_LINK_REVIEW_MOBILE","UPDATE WH 219 SET RefrenceId=26"
"236","32","69","09217672935","0","0","20/12/2025 10:38:43.333333","43","SP","43","manualy44","56000000","115","HINT_TX_USER_MISMATCH_WALLET","UPDATE WH 236 SET RefrenceId=43"
"245","34","71","09015793338","0","0","20/12/2025 10:39:58.833333","52","SP","52","localwallet-533cd27c-9ff5-497b-b23d-68df374c1107","37035000",,"HINT_TX_NOT_MANUALY","Tx from hint missing — may need manual charge review"
"246","37","74","09101692049","0","0","20/12/2025 10:39:58.833333","53","SP","53","C4_N9894294","56000000","88","HINT_TX_NOT_MANUALY","Tx from hint missing — may need manual charge review"
"23","manualy24","24",,,,"SUFFIX_WH_ID_GONE"
"24","manualy25","25",,,,"SUFFIX_WH_ID_GONE"
"25","manualy26","26",,,,"SUFFIX_WH_ID_GONE"
"26","manualy27","27",,,,"SUFFIX_WH_ID_GONE"
"28","manualy29","29",,,,"SUFFIX_WH_ID_GONE"
"29","manualy30","30",,,,"SUFFIX_WH_ID_GONE"
"30","manualy31","31",,,,"SUFFIX_WH_ID_GONE"
"31","manualy32","32",,,,"SUFFIX_WH_ID_GONE"
"42","manualy43","43",,,,"SUFFIX_WH_ID_GONE"
"43","manualy44","44",,,,"SUFFIX_WH_ID_GONE"
"44","manualy45","45",,,,"SUFFIX_WH_ID_GONE"
"45","manualy46","46",,,,"SUFFIX_WH_ID_GONE"
"46","manualy47","47",,,,"SUFFIX_WH_ID_GONE"
"56","manualy57","57",,,,"SUFFIX_WH_ID_GONE"
"57","manualy58","58",,,,"SUFFIX_WH_ID_GONE"
"58","manualy59","59",,,,"SUFFIX_WH_ID_GONE"
"59","manualy60","60",,,,"SUFFIX_WH_ID_GONE"
"68","manualy69","69",,,,"SUFFIX_WH_ID_GONE"
"107","manualy292","292","292","107","118","SUFFIX_WH_LINKED_OK"
"108","manualy293","293","293","108","119","SUFFIX_WH_LINKED_OK"
"109","manualy294","294","294","109","120","SUFFIX_WH_LINKED_OK"
"110","manualy295","295","295","110","123","SUFFIX_WH_LINKED_OK"
"7","09128676640","مهدی","صیفی","3","30","WH_REF, WH_REF, WH_REF","0","0"
"9","09387095611","مهدی","مرجانی","3","31","WH_REF, WH_REF, WH_REF","0","0"
"43","09039033341","کریم","خادمی ","3","29","WH_REF, WH_REF, WH_REF","0","0"
"45","09121150062","علی","دلال زاده","3","30","WH_REF, WH_REF","0","0"
"47","09123122544","صدیقه","منعمیان","3","23","WH_REF, WH_REF, WH_REF, WH_REF","0","0"
"50","09207316560","مرتضی ","اینالو","1","24","WH_REF, WH_REF","0","1"
"52","09178327020","حمیدرضا ","اسمعیلی ","3","31","WH_REF","0","0"
"69","09217672935","مهدی ","گیوی ","3","43","TX_MOBILE, WH_CREATEDBY_HINT, WH_CREATEDBY_HINT","0","0"
"71","09015793338","حمیدرضا","مرجانی","3","25","TX_MOBILE, WH_CREATEDBY_HINT, WH_CREATEDBY_HINT, WH_REF","0","0"
"72","09124678697","مژگان","گیوی","3","57","WH_CREATEDBY_HINT","0","0"
"74","09101692049","سجاد ","اداوی","3","26","TX_MOBILE, WH_CREATEDBY_HINT, WH_CREATEDBY_HINT","0","0"
"75","09353851578","پویا","عبدی ","3","23","WH_REF, WH_REF, WH_REF","0","0"
"88","09172606097","کریم","رعیت پیشه","1","28","WH_REF","1","0"
"93","09122156286","ریحانه سادات ","هاشمی نصر ","3","24","WH_REF","0","0"
"96","09125020729","غلامرضا ","عبدی ","3","25","WH_REF","0","0"
"99","09106519822","هستی","خادمی","3","26","WH_REF","0","0"
"103","09384366040","علی ","دلال زاده","3","46","TX_MOBILE, WH_REF","0","0"
"111","09352828086","انسیه","توسلیان","1","109","WH_REF","0","0"
"113","09177325630","کاوس","بیگ اینالو","3","45","TX_MOBILE, WH_REF","0","0"
"114","09397939699","اکرم","دهقانی","1","29","WH_REF, WH_REF","1","0"
"115","09056365323","حامد","دهقانی","3","43","TX_MOBILE, WH_REF","0","0"
"116","09014638673","سالار","رضائی","3","42","TX_MOBILE, WH_REF","0","0"
"119","09126092994","علیرضا ","کریمی پیروز ","3","44","TX_MOBILE, WH_REF","0","0"
"142","09399172812","صغری","شبانکاره ","3","68","TX_MOBILE","0","0"
"170","09120991323","لیلا","خدارحمی دره میانه","3","107","WH_REF","0","0"
"171","09375724408","مژده","بنی هاشمی","3","108","WH_REF","0","0"
"172","09121355832","مهرافشان","زاهدنیا","3","109","WH_REF","0","0"
"175","09122332219","ناهید ","حسن زاده مژدهی","3","110","WH_REF","0","0"
"186","09395618852","احمد","عالم","2","110","WH_REF","0","2"
1 TransactionId RefId OriginalChangeLogId Amount PaymentDate TxCreatedBy TxLastModifiedBy UserByMobile UserByMobilePhone UserByMobileFirst UserByMobileLast WhIdViaRef WhWalletViaRef UserViaWhRef WhIdViaCreatedByHint WhWalletViaHint UserViaWhHint PrimaryLink LinkQuality
2 23 manualy24 24 56000000 19/11/2025 01:24:24 102 38 75 WH_REFRENCE_ID OK
3 23 manualy24 24 56000000 19/11/2025 01:24:24 216 24 47 WH_REFRENCE_ID OK
4 24 manualy25 25 56000000 19/11/2025 01:24:24 103 46 93 WH_REFRENCE_ID OK
5 24 manualy25 25 56000000 19/11/2025 01:24:24 217 6 50 WH_REFRENCE_ID OK
6 25 manualy26 26 56000000 19/11/2025 01:24:24 104 47 96 218 34 71 WH_REFRENCE_ID CONFLICT_WH_REF_VS_HINT
7 26 manualy27 27 56000000 19/11/2025 01:24:24 105 49 99 219 37 74 WH_REFRENCE_ID CONFLICT_WH_REF_VS_HINT
8 28 manualy29 29 56000000 19/11/2025 01:24:24 106 24 47 WH_REFRENCE_ID OK
9 28 manualy29 29 56000000 19/11/2025 01:24:24 221 42 88 WH_REFRENCE_ID OK
10 29 manualy30 30 56000000 19/11/2025 01:24:24 107 22 43 WH_REFRENCE_ID OK
11 29 manualy30 30 56000000 19/11/2025 01:24:24 222 62 114 WH_REFRENCE_ID OK
12 30 manualy31 31 56000000 19/11/2025 01:24:24 108 23 45 WH_REFRENCE_ID OK
13 30 manualy31 31 56000000 19/11/2025 01:24:24 223 13 7 WH_REFRENCE_ID OK
14 31 manualy32 32 56000000 28/11/2025 05:27:38 122 26 52 WH_REFRENCE_ID OK
15 31 manualy32 32 56000000 28/11/2025 05:27:38 224 15 9 WH_REFRENCE_ID OK
16 42 manualy43 43 56000000 3/12/2025 09:49:45.92 09014638673 09014638673 116 09014638673 سالار رضائی 138 64 116 WH_REFRENCE_ID OK
17 42 manualy43 43 56000000 3/12/2025 09:49:45.92 09014638673 09014638673 116 09014638673 سالار رضائی 235 24 47 WH_REFRENCE_ID CONFLICT_WH_REF_VS_MOBILE
18 43 manualy44 44 56000000 3/12/2025 09:49:45.92 09056365323 09056365323 115 09056365323 حامد دهقانی 139 63 115 236 32 69 WH_REFRENCE_ID CONFLICT_WH_REF_VS_HINT
19 44 manualy45 45 56000000 3/12/2025 09:49:45.92 09126092994 09126092994 119 09126092994 علیرضا کریمی پیروز 140 67 119 WH_REFRENCE_ID OK
20 44 manualy45 45 56000000 3/12/2025 09:49:45.92 09126092994 09126092994 119 09126092994 علیرضا کریمی پیروز 237 38 75 WH_REFRENCE_ID CONFLICT_WH_REF_VS_MOBILE
21 45 manualy46 46 56000000 3/12/2025 09:49:45.92 09177325630 09177325630 113 09177325630 کاوس بیگ اینالو 141 61 113 WH_REFRENCE_ID OK
22 45 manualy46 46 56000000 3/12/2025 09:49:45.92 09177325630 09177325630 113 09177325630 کاوس بیگ اینالو 238 6 50 WH_REFRENCE_ID CONFLICT_WH_REF_VS_MOBILE
23 46 manualy47 47 56000000 3/12/2025 09:49:45.92 09384366040 09384366040 103 09384366040 علی دلال زاده 142 51 103 WH_REFRENCE_ID OK
24 46 manualy47 47 56000000 3/12/2025 09:49:45.92 09384366040 09384366040 103 09384366040 علی دلال زاده 239 13 7 WH_REFRENCE_ID CONFLICT_WH_REF_VS_MOBILE
25 56 manualy57 57 56000000 3/12/2025 09:49:45.92 09015793338 09015793338 71 09015793338 حمیدرضا مرجانی 249 62 114 118 34 71 WH_REFRENCE_ID CONFLICT_WH_REF_VS_HINT
26 57 manualy58 58 56000000 19/11/2025 01:24:24 09961354257 09961354257 250 13 7 124 35 72 WH_REFRENCE_ID CONFLICT_WH_REF_VS_HINT
27 58 manualy59 59 56000000 19/11/2025 01:24:24 09217672935 09217672935 69 09217672935 مهدی گیوی 251 15 9 119 32 69 WH_REFRENCE_ID CONFLICT_WH_REF_VS_HINT
28 59 manualy60 60 56000000 19/11/2025 01:24:24 09101692049 09101692049 74 09101692049 سجاد اداوی 252 22 43 120 37 74 WH_REFRENCE_ID CONFLICT_WH_REF_VS_HINT
29 68 manualy69 69 56000000 8/12/2025 12:30:53.536666 09399172812 09399172812 142 09399172812 صغری شبانکاره TX_MOBILE_ONLY OK
30 107 manualy292 292 56000000 30/12/2025 01:24:24 186 15 9 WH_REFRENCE_ID OK
31 107 manualy292 292 56000000 30/12/2025 01:24:24 292 118 170 WH_REFRENCE_ID OK
32 107 manualy292 292 56000000 30/12/2025 01:24:24 40399 34 71 WH_REFRENCE_ID OK
33 108 manualy293 293 56000000 31/12/2025 01:24:24 187 22 43 WH_REFRENCE_ID OK
34 108 manualy293 293 56000000 31/12/2025 01:24:24 293 119 171 WH_REFRENCE_ID OK
35 108 manualy293 293 56000000 31/12/2025 01:24:24 40400 38 75 WH_REFRENCE_ID OK
36 109 manualy294 294 56000000 31/12/2025 01:24:24 188 23 45 WH_REFRENCE_ID OK
37 109 manualy294 294 56000000 31/12/2025 01:24:24 294 120 172 WH_REFRENCE_ID OK
38 109 manualy294 294 56000000 31/12/2025 01:24:24 40401 59 111 WH_REFRENCE_ID OK
39 110 manualy295 295 56000000 1/1/2026 17:07:53.2301978 189 24 47 WH_REFRENCE_ID OK
40 110 manualy295 295 56000000 1/1/2026 17:07:53.2301978 295 123 175 WH_REFRENCE_ID OK
41 110 manualy295 295 56000000 1/1/2026 17:07:53.2301978 40402 134 186 WH_REFRENCE_ID OK
42 118 34 71 09015793338 0 1 3/12/2025 09:49:45.92 56 09015793338 56 manualy57 56000000 71 FIX_LINK_WH_TO_HINT_TX UPDATE WH 118 SET RefrenceId=56
43 119 32 69 09217672935 0 1 19/11/2025 01:24:24 58 09217672935 58 manualy59 56000000 69 FIX_LINK_WH_TO_HINT_TX UPDATE WH 119 SET RefrenceId=58
44 120 37 74 09101692049 0 1 19/11/2025 01:24:24 59 09101692049 59 manualy60 56000000 74 FIX_LINK_WH_TO_HINT_TX UPDATE WH 120 SET RefrenceId=59
45 121 12 2 09387342688 0 1 19/12/2025 22:00:40.276666 0 09387342688 NO_NUMERIC_HINT Cannot auto-link — no CreatedBy Tx hint
46 124 35 72 09124678697 0 1 19/11/2025 01:24:24 57 09961354257 57 manualy58 56000000 FIX_LINK_REVIEW_MOBILE UPDATE WH 124 SET RefrenceId=57
47 201 110 162 09199877503 0 1 20/12/2025 02:24:42.771904 92 162 HINT_POINTS_TO_MISSING_TX Tx from hint missing — may need manual charge review
48 209 32 69 09217672935 0 0 20/12/2025 10:30:49.186666 16 SP 16 localwallet-d0870aee-8a21-4cfc-a5a0-362b25795af0 10000 HINT_TX_NOT_MANUALY Tx from hint missing — may need manual charge review
49 218 34 71 09015793338 0 0 20/12/2025 10:31:12.52 25 SP 25 manualy26 56000000 FIX_LINK_REVIEW_MOBILE UPDATE WH 218 SET RefrenceId=25
50 219 37 74 09101692049 0 0 20/12/2025 10:31:12.52 26 SP 26 manualy27 56000000 FIX_LINK_REVIEW_MOBILE UPDATE WH 219 SET RefrenceId=26
51 236 32 69 09217672935 0 0 20/12/2025 10:38:43.333333 43 SP 43 manualy44 56000000 115 HINT_TX_USER_MISMATCH_WALLET UPDATE WH 236 SET RefrenceId=43
52 245 34 71 09015793338 0 0 20/12/2025 10:39:58.833333 52 SP 52 localwallet-533cd27c-9ff5-497b-b23d-68df374c1107 37035000 HINT_TX_NOT_MANUALY Tx from hint missing — may need manual charge review
53 246 37 74 09101692049 0 0 20/12/2025 10:39:58.833333 53 SP 53 C4_N9894294 56000000 88 HINT_TX_NOT_MANUALY Tx from hint missing — may need manual charge review
54 23 manualy24 24 SUFFIX_WH_ID_GONE
55 24 manualy25 25 SUFFIX_WH_ID_GONE
56 25 manualy26 26 SUFFIX_WH_ID_GONE
57 26 manualy27 27 SUFFIX_WH_ID_GONE
58 28 manualy29 29 SUFFIX_WH_ID_GONE
59 29 manualy30 30 SUFFIX_WH_ID_GONE
60 30 manualy31 31 SUFFIX_WH_ID_GONE
61 31 manualy32 32 SUFFIX_WH_ID_GONE
62 42 manualy43 43 SUFFIX_WH_ID_GONE
63 43 manualy44 44 SUFFIX_WH_ID_GONE
64 44 manualy45 45 SUFFIX_WH_ID_GONE
65 45 manualy46 46 SUFFIX_WH_ID_GONE
66 46 manualy47 47 SUFFIX_WH_ID_GONE
67 56 manualy57 57 SUFFIX_WH_ID_GONE
68 57 manualy58 58 SUFFIX_WH_ID_GONE
69 58 manualy59 59 SUFFIX_WH_ID_GONE
70 59 manualy60 60 SUFFIX_WH_ID_GONE
71 68 manualy69 69 SUFFIX_WH_ID_GONE
72 107 manualy292 292 292 107 118 SUFFIX_WH_LINKED_OK
73 108 manualy293 293 293 108 119 SUFFIX_WH_LINKED_OK
74 109 manualy294 294 294 109 120 SUFFIX_WH_LINKED_OK
75 110 manualy295 295 295 110 123 SUFFIX_WH_LINKED_OK
76 7 09128676640 مهدی صیفی 3 30 WH_REF, WH_REF, WH_REF 0 0
77 9 09387095611 مهدی مرجانی 3 31 WH_REF, WH_REF, WH_REF 0 0
78 43 09039033341 کریم خادمی 3 29 WH_REF, WH_REF, WH_REF 0 0
79 45 09121150062 علی دلال زاده 3 30 WH_REF, WH_REF 0 0
80 47 09123122544 صدیقه منعمیان 3 23 WH_REF, WH_REF, WH_REF, WH_REF 0 0
81 50 09207316560 مرتضی اینالو 1 24 WH_REF, WH_REF 0 1
82 52 09178327020 حمیدرضا اسمعیلی 3 31 WH_REF 0 0
83 69 09217672935 مهدی گیوی 3 43 TX_MOBILE, WH_CREATEDBY_HINT, WH_CREATEDBY_HINT 0 0
84 71 09015793338 حمیدرضا مرجانی 3 25 TX_MOBILE, WH_CREATEDBY_HINT, WH_CREATEDBY_HINT, WH_REF 0 0
85 72 09124678697 مژگان گیوی 3 57 WH_CREATEDBY_HINT 0 0
86 74 09101692049 سجاد اداوی 3 26 TX_MOBILE, WH_CREATEDBY_HINT, WH_CREATEDBY_HINT 0 0
87 75 09353851578 پویا عبدی 3 23 WH_REF, WH_REF, WH_REF 0 0
88 88 09172606097 کریم رعیت پیشه 1 28 WH_REF 1 0
89 93 09122156286 ریحانه سادات هاشمی نصر 3 24 WH_REF 0 0
90 96 09125020729 غلامرضا عبدی 3 25 WH_REF 0 0
91 99 09106519822 هستی خادمی 3 26 WH_REF 0 0
92 103 09384366040 علی دلال زاده 3 46 TX_MOBILE, WH_REF 0 0
93 111 09352828086 انسیه توسلیان 1 109 WH_REF 0 0
94 113 09177325630 کاوس بیگ اینالو 3 45 TX_MOBILE, WH_REF 0 0
95 114 09397939699 اکرم دهقانی 1 29 WH_REF, WH_REF 1 0
96 115 09056365323 حامد دهقانی 3 43 TX_MOBILE, WH_REF 0 0
97 116 09014638673 سالار رضائی 3 42 TX_MOBILE, WH_REF 0 0
98 119 09126092994 علیرضا کریمی پیروز 3 44 TX_MOBILE, WH_REF 0 0
99 142 09399172812 صغری شبانکاره 3 68 TX_MOBILE 0 0
100 170 09120991323 لیلا خدارحمی دره میانه 3 107 WH_REF 0 0
101 171 09375724408 مژده بنی هاشمی 3 108 WH_REF 0 0
102 172 09121355832 مهرافشان زاهدنیا 3 109 WH_REF 0 0
103 175 09122332219 ناهید حسن زاده مژدهی 3 110 WH_REF 0 0
104 186 09395618852 احمد عالم 2 110 WH_REF 0 2
@@ -1,90 +0,0 @@
-- =============================================
-- Diagnose: Leg counts — all subtree vs active-only vs gold/silver
-- Usage: SET @RootUserId to the tree owner UserId
-- =============================================
DECLARE @RootUserId BIGINT = 1; -- ← change to target user
DECLARE @GoldPackageId BIGINT = (
SELECT TOP 1 Id FROM CMS.Packages
WHERE IsDeleted = 0 AND IsActive = 1 AND IsBasePackage = 1
ORDER BY Id
);
DECLARE @SilverPackageId BIGINT = (
SELECT TOP 1 Id FROM CMS.Packages
WHERE IsDeleted = 0 AND IsActive = 1 AND IsBasePackage = 0
AND (Title LIKE N'%نقر%' OR MaxBalancesPerLeg <= 30)
ORDER BY Id
);
DECLARE @LeftChildId BIGINT = (
SELECT Id FROM CMS.Users
WHERE NetworkParentId = @RootUserId AND LegPosition = 0 AND IsDeleted = 0
);
DECLARE @RightChildId BIGINT = (
SELECT Id FROM CMS.Users
WHERE NetworkParentId = @RootUserId AND LegPosition = 1 AND IsDeleted = 0
);
;WITH LegUsers AS (
SELECT @LeftChildId AS RootLegUserId, 0 AS LegSide
WHERE @LeftChildId IS NOT NULL
UNION ALL
SELECT @RightChildId, 1
WHERE @RightChildId IS NOT NULL
),
Subtree AS (
SELECT lu.RootLegUserId, lu.LegSide, lu.RootLegUserId AS UserId
FROM LegUsers lu
UNION ALL
SELECT s.RootLegUserId, s.LegSide, u.Id
FROM Subtree s
INNER JOIN CMS.Users u ON u.NetworkParentId = s.UserId AND u.IsDeleted = 0
),
AllSubtreeCounts AS (
SELECT LegSide, COUNT(DISTINCT UserId) AS Cnt
FROM Subtree
GROUP BY LegSide
),
ActiveClubCounts AS (
SELECT s.LegSide, COUNT(DISTINCT s.UserId) AS Cnt
FROM Subtree s
INNER JOIN CMS.ClubMemberships cm
ON cm.UserId = s.UserId AND cm.IsActive = 1 AND cm.IsDeleted = 0
GROUP BY s.LegSide
),
ActivePackageHolders AS (
SELECT
s.LegSide,
cc.PackageId,
COUNT(DISTINCT s.UserId) AS HolderCount
FROM Subtree s
INNER JOIN CMS.ClubMemberships cm ON cm.UserId = s.UserId AND cm.IsActive = 1 AND cm.IsDeleted = 0
INNER JOIN CMS.ClubMembershipCycles cc ON cc.UserId = s.UserId AND cc.IsCurrentCycle = 1
GROUP BY s.LegSide, cc.PackageId
)
SELECT
@RootUserId AS RootUserId,
-- All subtree (old سازمان چپ/راست)
ISNULL((SELECT Cnt FROM AllSubtreeCounts WHERE LegSide = 0), 0) AS AllLeft,
ISNULL((SELECT Cnt FROM AllSubtreeCounts WHERE LegSide = 1), 0) AS AllRight,
-- Active club only (new سازمان چپ/راست / کل)
ISNULL((SELECT Cnt FROM ActiveClubCounts WHERE LegSide = 0), 0) AS ActiveLeft,
ISNULL((SELECT Cnt FROM ActiveClubCounts WHERE LegSide = 1), 0) AS ActiveRight,
ISNULL((SELECT Cnt FROM ActiveClubCounts WHERE LegSide = 0), 0)
+ ISNULL((SELECT Cnt FROM ActiveClubCounts WHERE LegSide = 1), 0) AS ActiveTotal,
-- Gold / Silver package holders (active + current cycle)
ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 0 AND PackageId = @GoldPackageId), 0) AS GoldLeft,
ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 1 AND PackageId = @GoldPackageId), 0) AS GoldRight,
ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 0 AND PackageId = @SilverPackageId), 0) AS SilverLeft,
ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 1 AND PackageId = @SilverPackageId), 0) AS SilverRight,
-- Gaps: Active − (Gold+Silver) per leg
ISNULL((SELECT Cnt FROM ActiveClubCounts WHERE LegSide = 0), 0)
- ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 0 AND PackageId = @GoldPackageId), 0)
- ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 0 AND PackageId = @SilverPackageId), 0) AS GapLeft_ActiveMinusPackages,
ISNULL((SELECT Cnt FROM ActiveClubCounts WHERE LegSide = 1), 0)
- ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 1 AND PackageId = @GoldPackageId), 0)
- ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 1 AND PackageId = @SilverPackageId), 0) AS GapRight_ActiveMinusPackages
OPTION (MAXRECURSION 0);
@@ -1,116 +0,0 @@
/*
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
@@ -1,145 +0,0 @@
/*
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.';
@@ -1,540 +0,0 @@
/*
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 + ledger WH)
If ExistingDayaTxId found (wallet RefrenceId → Tx Description LIKE '%اعتبار دایا%'
OR Tx.RefId = ContractNumber): REUSE that Tx — do NOT create Transaction.
NO wallet Balance/DiscountBalance charge — UserWalletHistory is ledger-only when needed.
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;
------------------------------------------------------------
-- PREVIEW: ledger actions (no wallet charge)
------------------------------------------------------------
PRINT '=== LEDGER PREVIEW (Tx + WH history-only — NO wallet charge) ===';
SELECT
w.ContractNumber,
w.NationalCode,
w.UserId,
w.Verdict,
w.HasCredit,
w.ExistingDayaTxId,
CASE
WHEN w.UserId IS NULL THEN N'SKIP_NO_USER'
WHEN w.ExistingDayaTxId IS NOT NULL THEN N'REUSE_TX_ENSURE_WH_UPP'
WHEN w.HasCredit = 0 THEN N'CREATE_TX_WH_UPP'
ELSE N'SET_FLAGS_ONLY_UPP'
END AS LedgerAction
FROM @Work w
WHERE w.Verdict IN (N'NOT_PROCESSED', N'PARTIAL')
ORDER BY LedgerAction, w.ContractNumber;
SELECT
CASE
WHEN w.UserId IS NULL THEN N'SKIP_NO_USER'
WHEN w.ExistingDayaTxId IS NOT NULL THEN N'REUSE_TX_ENSURE_WH_UPP'
WHEN w.HasCredit = 0 THEN N'CREATE_TX_WH_UPP'
ELSE N'SET_FLAGS_ONLY_UPP'
END AS LedgerAction,
COUNT(*) AS Cnt
FROM @Work w
WHERE w.Verdict IN (N'NOT_PROCESSED', N'PARTIAL')
GROUP BY
CASE
WHEN w.UserId IS NULL THEN N'SKIP_NO_USER'
WHEN w.ExistingDayaTxId IS NOT NULL THEN N'REUSE_TX_ENSURE_WH_UPP'
WHEN w.HasCredit = 0 THEN N'CREATE_TX_WH_UPP'
ELSE N'SET_FLAGS_ONLY_UPP'
END
ORDER BY LedgerAction;
DECLARE @PreviewCreateTxCount INT = (
SELECT COUNT(*)
FROM @Work w
WHERE w.Verdict IN (N'NOT_PROCESSED', N'PARTIAL')
AND w.UserId IS NOT NULL
AND w.ExistingDayaTxId IS NULL
AND w.HasCredit = 0
);
PRINT CONCAT('>>> CREATE_TX (no wallet charge) count = ', @PreviewCreateTxCount);
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, @NetworkBalance BIGINT, @Action NVARCHAR(200);
DECLARE @CreateTxCount INT = 0;
DECLARE @ReuseCount INT = 0;
DECLARE @SkipFlagsOnlyCount INT = 0;
DECLARE @WhCreatedCount INT = 0;
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
------------------------------------------------------------
-- Resolve Tx (reuse existing or create ledger Tx — NO wallet charge)
------------------------------------------------------------
IF @ExistingDayaTxId IS NOT NULL
BEGIN
SET @TxId = @ExistingDayaTxId;
SET @Action = @Action + CONCAT(N'REUSE_EXISTING_DAYA_TX=', @TxId, N';');
SET @ReuseCount = @ReuseCount + 1;
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;';
END
ELSE IF @HasCredit = 0
BEGIN
INSERT INTO CMS.Transactions
(
Amount, Description, PaymentStatus, PaymentDate, RefId, Type,
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
VALUES
(
@PackagePrice,
CONCAT(N'دریافت اعتبار دایا - قرارداد ', @ContractNumber),
0,
@Now,
@ContractNumber,
2,
@Now, NULL, @Now, NULL, 0
);
SET @TxId = SCOPE_IDENTITY();
SET @CreateTxCount = @CreateTxCount + 1;
UPDATE CMS.Users
SET
HasReceivedDayaCredit = 1,
DayaCreditReceivedAt = @Now,
PackagePurchaseMethod = 1,
LastModified = @Now
WHERE Id = @UserId;
SET @HasCredit = 1;
SET @Action = @Action + N'CREATE_TX+USER_FLAGS;';
END
ELSE
BEGIN
UPDATE CMS.Users
SET
PackagePurchaseMethod = 1,
HasReceivedDayaCredit = 1,
DayaCreditReceivedAt = COALESCE(DayaCreditReceivedAt, @Now),
LastModified = @Now
WHERE Id = @UserId;
SET @SkipFlagsOnlyCount = @SkipFlagsOnlyCount + 1;
SET @Action = @Action + N'SET_FLAGS_ONLY;';
END
------------------------------------------------------------
-- UserWalletHistory for Tx (ledger-only — NO Balance/Discount charge)
------------------------------------------------------------
IF @TxId IS NOT NULL
BEGIN
SELECT @WalletId = Id, @NetworkBalance = COALESCE(NetworkBalance, 0)
FROM CMS.UserWallets
WHERE UserId = @UserId AND IsDeleted = 0;
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();
SET @NetworkBalance = 0;
END
IF NOT EXISTS (
SELECT 1
FROM CMS.UserWalletHistories h
WHERE h.WalletId = @WalletId
AND h.IsDeleted = 0
AND h.RefrenceId = @TxId
)
BEGIN
INSERT INTO CMS.UserWalletHistories
(
WalletId, CurrentBalance, ChangeValue, CurrentNetworkBalance, ChangeNerworkValue,
CurrentDiscountBalance, ChangeDiscountValue, IsIncrease, RefrenceId, PackageId,
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
VALUES
(
@WalletId, 0, @PackagePrice, @NetworkBalance, 0,
0, @DiscountAmount, 1, @TxId, @PackageId,
@Now, NULL, @Now, NULL, 0
);
SET @WhCreatedCount = @WhCreatedCount + 1;
SET @Action = @Action + N'INSERT_WH_HISTORY;';
END
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 '=== LEDGER SUMMARY (no wallet charge) ===';
PRINT CONCAT('CREATE_TX count = ', @CreateTxCount);
PRINT CONCAT('REUSE_TX count = ', @ReuseCount);
PRINT CONCAT('SET_FLAGS_ONLY count = ', @SkipFlagsOnlyCount);
PRINT CONCAT('WH history created = ', @WhCreatedCount);
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
@@ -1,547 +0,0 @@
/*
Fix users wrongly marked DayaLoan who were manually charged (RefId LIKE 'manualy%').
Scope: ONLY users linked to manualy transactions — NOT the full 47-user WrongDaya list.
Real Daya users (RefId LIKE 'C4%') are excluded automatically.
Actions:
- PurchaseMethod 1→3 on Users / Club / Cycle / UPP
- Clear HasReceivedDayaCredit / DayaCreditReceivedAt
- Soft-delete stub DayaLoanContracts (unprocessed, no Tx)
- Fix broken WH.RefrenceId where CreatedBy hint points to existing manualy Tx
- Create missing UPP Manual(3) reusing existing manualy Tx (never create new Tx)
- Create missing WH rows only when Tx exists but no WH links to it (history-only, no charge)
RefrenceId=0 rule:
Tx is resolved ONLY via numeric CreatedBy hint when that Tx exists with RefId manualy%.
Mobile-only or manualy{N} suffix alone is NOT used to assume a Tx exists.
PackagePurchaseMethod: None=0, DayaLoan=1, DirectPurchase=2, Manual=3
Defaults to ROLLBACK. Uncomment COMMIT after verifying PRINT output.
Run 10-Diagnose_Manualy_Transaction_Map.sql first.
*/
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRAN;
DECLARE @Now DATETIME2 = SYSUTCDATETIME();
DECLARE @BasePackageId BIGINT;
DECLARE @BasePackagePrice BIGINT;
SELECT TOP (1)
@BasePackageId = Id,
@BasePackagePrice = Price
FROM CMS.Packages
WHERE IsDeleted = 0 AND IsBasePackage = 1
ORDER BY Id;
IF @BasePackageId IS NULL
THROW 50001, 'Base package not found.', 1;
------------------------------------------------------------
-- Scope: users linked to manualy transactions
-- (duplicate WH→Tx links exist from migration; rank by mobile match + charge amount + date)
------------------------------------------------------------
DECLARE @ManualyUsers TABLE (
UserId BIGINT NOT NULL PRIMARY KEY,
ManualyTxId BIGINT NULL,
LinkSource NVARCHAR(50) NULL
);
SELECT t.Id AS TxId, t.RefId, t.Amount, t.PaymentDate, t.CreatedBy, t.LastModifiedBy
INTO #ManualyTx
FROM CMS.Transactions t
WHERE t.IsDeleted = 0 AND t.RefId LIKE N'manualy%';
SELECT
mt.TxId,
w.UserId,
h.Id AS WhId,
h.ChangeValue,
CASE
WHEN NULLIF(LTRIM(RTRIM(mt.CreatedBy)), N'') LIKE N'09%'
AND u.Mobile = NULLIF(LTRIM(RTRIM(mt.CreatedBy)), N'') THEN 1
WHEN NULLIF(LTRIM(RTRIM(mt.LastModifiedBy)), N'') LIKE N'09%'
AND u.Mobile = NULLIF(LTRIM(RTRIM(mt.LastModifiedBy)), N'') THEN 1
ELSE 0
END AS MobileMatch,
CASE WHEN h.ChangeValue >= mt.Amount THEN 1 ELSE 0 END AS FullCharge,
ABS(DATEDIFF(SECOND, h.Created, mt.PaymentDate)) AS TimeDeltaSec
INTO #WhLinks
FROM #ManualyTx mt
INNER JOIN CMS.UserWalletHistories h ON h.RefrenceId = mt.TxId AND h.IsDeleted = 0
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
INNER JOIN CMS.Users u ON u.Id = w.UserId AND u.IsDeleted = 0;
SELECT UserId, TxId, WhId, MobileMatch, FullCharge,
ROW_NUMBER() OVER (
PARTITION BY TxId
ORDER BY MobileMatch DESC, FullCharge DESC, TimeDeltaSec ASC, WhId ASC
) AS rn
INTO #BestWhPerTx
FROM #WhLinks;
INSERT INTO @ManualyUsers (UserId, ManualyTxId, LinkSource)
SELECT b.UserId, b.TxId, N'WH_REF_BEST'
FROM #BestWhPerTx b
WHERE b.rn = 1
AND (b.MobileMatch = 1 OR b.FullCharge = 1);
INSERT INTO @ManualyUsers (UserId, ManualyTxId, LinkSource)
SELECT DISTINCT w.UserId, t.Id, N'WH_CREATEDBY_HINT'
FROM CMS.UserWalletHistories h
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
INNER JOIN CMS.Users u ON u.Id = w.UserId AND u.IsDeleted = 0
WHERE h.IsDeleted = 0
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0
AND (
NULLIF(LTRIM(RTRIM(t.CreatedBy)), N'') NOT LIKE N'09%'
OR u.Mobile IN (
NULLIF(LTRIM(RTRIM(t.CreatedBy)), N''),
NULLIF(LTRIM(RTRIM(t.LastModifiedBy)), N'')
)
)
AND NOT EXISTS (SELECT 1 FROM @ManualyUsers m WHERE m.UserId = w.UserId);
INSERT INTO @ManualyUsers (UserId, ManualyTxId, LinkSource)
SELECT DISTINCT u.Id, t.Id, N'TX_MOBILE'
FROM CMS.Transactions t
INNER JOIN CMS.Users u ON u.IsDeleted = 0
AND u.Mobile IN (
NULLIF(LTRIM(RTRIM(t.CreatedBy)), N''),
NULLIF(LTRIM(RTRIM(t.LastModifiedBy)), N'')
)
WHERE t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
AND NOT EXISTS (SELECT 1 FROM @ManualyUsers m WHERE m.UserId = u.Id);
-- Txs without mobile on row: keep best full-charge WH only (early batch 23-31)
INSERT INTO @ManualyUsers (UserId, ManualyTxId, LinkSource)
SELECT b.UserId, b.TxId, N'WH_REF_NO_MOBILE'
FROM #BestWhPerTx b
INNER JOIN #ManualyTx mt ON mt.TxId = b.TxId
WHERE b.rn = 1
AND b.FullCharge = 1
AND (NULLIF(LTRIM(RTRIM(mt.CreatedBy)), N'') IS NULL OR NULLIF(LTRIM(RTRIM(mt.CreatedBy)), N'') NOT LIKE N'09%')
AND (NULLIF(LTRIM(RTRIM(mt.LastModifiedBy)), N'') IS NULL OR NULLIF(LTRIM(RTRIM(mt.LastModifiedBy)), N'') NOT LIKE N'09%')
AND NOT EXISTS (SELECT 1 FROM @ManualyUsers m WHERE m.UserId = b.UserId);
DROP TABLE #BestWhPerTx;
DROP TABLE #WhLinks;
DROP TABLE #ManualyTx;
DECLARE @ScopeCount INT = (SELECT COUNT(*) FROM @ManualyUsers);
PRINT CONCAT('manualy scope users: ', @ScopeCount);
PRINT CONCAT('BasePackageId=', @BasePackageId, ' Price=', @BasePackagePrice);
SELECT mu.UserId, u.Mobile, u.FirstName, u.LastName,
u.PackagePurchaseMethod, mu.ManualyTxId, mu.LinkSource
FROM @ManualyUsers mu
INNER JOIN CMS.Users u ON u.Id = mu.UserId AND u.IsDeleted = 0
ORDER BY mu.UserId;
------------------------------------------------------------
-- Preview
------------------------------------------------------------
SELECT 'Users with manualy Tx resolved' AS CheckName, COUNT(*) AS Cnt
FROM @ManualyUsers WHERE ManualyTxId IS NOT NULL
UNION ALL
SELECT 'Users in scope without resolved Tx', COUNT(*)
FROM @ManualyUsers WHERE ManualyTxId IS NULL
UNION ALL
SELECT 'Broken WH rows fixable via CreatedBy hint', COUNT(*)
FROM CMS.UserWalletHistories h
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
INNER JOIN @ManualyUsers mu ON mu.UserId = w.UserId
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
WHERE h.IsDeleted = 0
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0
UNION ALL
SELECT 'Missing UPP in scope', COUNT(*)
FROM @ManualyUsers mu
WHERE NOT EXISTS (
SELECT 1 FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = mu.UserId AND upp.IsDeleted = 0
);
------------------------------------------------------------
-- 1) Fix broken WH.RefrenceId (CreatedBy hint → existing manualy Tx)
------------------------------------------------------------
UPDATE h
SET
h.RefrenceId = t.Id,
h.LastModified = @Now
FROM CMS.UserWalletHistories h
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
INNER JOIN @ManualyUsers mu ON mu.UserId = w.UserId
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
AND t.IsDeleted = 0
AND t.RefId LIKE N'manualy%'
AND t.PaymentStatus = 0
WHERE h.IsDeleted = 0
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0;
PRINT CONCAT('WalletHistory RefrenceId fixed: ', @@ROWCOUNT);
------------------------------------------------------------
-- 2) Users — DayaLoan → Manual
------------------------------------------------------------
UPDATE u
SET
u.PackagePurchaseMethod = 3,
u.HasReceivedDayaCredit = 0,
u.DayaCreditReceivedAt = NULL,
u.LastModified = @Now
FROM CMS.Users u
INNER JOIN @ManualyUsers mu ON mu.UserId = u.Id
WHERE u.IsDeleted = 0
AND (
u.PackagePurchaseMethod = 1
OR u.HasReceivedDayaCredit = 1
OR u.DayaCreditReceivedAt IS NOT NULL
);
PRINT CONCAT('Users updated: ', @@ROWCOUNT);
------------------------------------------------------------
-- 3) ClubMemberships
------------------------------------------------------------
UPDATE m
SET m.PurchaseMethod = 3, m.LastModified = @Now
FROM CMS.ClubMemberships m
INNER JOIN @ManualyUsers mu ON mu.UserId = m.UserId
WHERE m.IsDeleted = 0 AND m.PurchaseMethod IN (0, 1);
PRINT CONCAT('ClubMemberships updated: ', @@ROWCOUNT);
------------------------------------------------------------
-- 4) ClubMembershipCycles
------------------------------------------------------------
UPDATE c
SET c.PurchaseMethod = 3, c.LastModified = @Now
FROM CMS.ClubMembershipCycles c
INNER JOIN @ManualyUsers mu ON mu.UserId = c.UserId
WHERE c.IsDeleted = 0 AND c.PurchaseMethod IN (0, 1);
PRINT CONCAT('ClubMembershipCycles updated: ', @@ROWCOUNT);
------------------------------------------------------------
-- 5) UserPackagePurchases (existing rows Daya → Manual)
------------------------------------------------------------
UPDATE upp
SET upp.PurchaseMethod = 3, upp.LastModified = @Now
FROM CMS.UserPackagePurchases upp
INNER JOIN @ManualyUsers mu ON mu.UserId = upp.UserId
WHERE upp.IsDeleted = 0 AND upp.PurchaseMethod = 1;
PRINT CONCAT('UserPackagePurchases updated: ', @@ROWCOUNT);
------------------------------------------------------------
-- 6) Soft-delete stub DayaLoanContracts
------------------------------------------------------------
UPDATE d
SET d.IsDeleted = 1, d.LastModified = @Now
FROM CMS.DayaLoanContracts d
INNER JOIN @ManualyUsers mu ON mu.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);
------------------------------------------------------------
-- 7) Resolve best manualy Tx per user (post WH fix)
------------------------------------------------------------
;WITH TxCandidates AS (
SELECT mu.UserId, t.Id AS TxId, N'WH_REF' AS Src, 1 AS Pri
FROM @ManualyUsers mu
INNER JOIN CMS.UserWallets w ON w.UserId = mu.UserId AND w.IsDeleted = 0
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
INNER JOIN CMS.Transactions t ON t.Id = h.RefrenceId
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%' AND t.PaymentStatus = 0
UNION ALL
SELECT mu.UserId, t.Id, N'WH_HINT', 2
FROM @ManualyUsers mu
INNER JOIN CMS.UserWallets w ON w.UserId = mu.UserId AND w.IsDeleted = 0
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%' AND t.PaymentStatus = 0
WHERE (h.RefrenceId IS NULL OR h.RefrenceId = 0)
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0
UNION ALL
SELECT mu.UserId, t.Id, N'TX_MOBILE', 3
FROM @ManualyUsers mu
INNER JOIN CMS.Users u ON u.Id = mu.UserId AND u.IsDeleted = 0
INNER JOIN CMS.Transactions t ON t.IsDeleted = 0
AND t.RefId LIKE N'manualy%' AND t.PaymentStatus = 0
AND u.Mobile IN (
NULLIF(LTRIM(RTRIM(t.CreatedBy)), N''),
NULLIF(LTRIM(RTRIM(t.LastModifiedBy)), N'')
)
UNION ALL
SELECT mu.UserId, mu.ManualyTxId, mu.LinkSource, 4
FROM @ManualyUsers mu
WHERE mu.ManualyTxId IS NOT NULL
),
BestTx AS (
SELECT UserId, TxId, Src,
ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY Pri, TxId DESC) AS rn
FROM TxCandidates
WHERE TxId IS NOT NULL
)
SELECT
mu.UserId,
bt.TxId,
bt.Src AS TxSource,
COALESCE(NULLIF(cm.LastPackageId, 0), NULLIF(cm.FirstPackageId, 0), @BasePackageId) AS PackageId,
COALESCE(t.Amount, @BasePackagePrice) AS Amount,
COALESCE(t.PaymentDate, cm.ActivatedAt, @Now) AS PurchasedAt
INTO #ManualyUppPlan
FROM @ManualyUsers mu
LEFT JOIN BestTx bt ON bt.UserId = mu.UserId AND bt.rn = 1
LEFT JOIN CMS.Transactions t ON t.Id = bt.TxId AND t.IsDeleted = 0
LEFT JOIN CMS.ClubMemberships cm ON cm.UserId = mu.UserId AND cm.IsDeleted = 0
WHERE NOT EXISTS (
SELECT 1 FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = mu.UserId AND upp.IsDeleted = 0
);
DECLARE @UppPlanCount INT = (SELECT COUNT(*) FROM #ManualyUppPlan);
PRINT CONCAT('UPP to create: ', @UppPlanCount);
SELECT UserId, TxId, TxSource, PackageId, Amount, PurchasedAt,
CASE WHEN TxId IS NULL THEN N'SKIP_NO_MANUALY_TX' ELSE N'CREATE_UPP' END AS PlanAction
FROM #ManualyUppPlan
ORDER BY UserId;
DECLARE @UserId BIGINT;
DECLARE @TxId BIGINT;
DECLARE @PackageId BIGINT;
DECLARE @Amount BIGINT;
DECLARE @PurchasedAt DATETIME2;
DECLARE @CreatedUpp INT = 0;
DECLARE @SkippedNoTx INT = 0;
DECLARE curUpp CURSOR LOCAL FAST_FORWARD FOR
SELECT UserId, TxId, PackageId, Amount, PurchasedAt
FROM #ManualyUppPlan;
OPEN curUpp;
FETCH NEXT FROM curUpp INTO @UserId, @TxId, @PackageId, @Amount, @PurchasedAt;
WHILE @@FETCH_STATUS = 0
BEGIN
IF @TxId IS NULL OR @PackageId IS NULL OR @Amount IS NULL OR @Amount <= 0
BEGIN
SET @SkippedNoTx = @SkippedNoTx + 1;
END
ELSE
BEGIN
INSERT INTO CMS.UserPackagePurchases
(
UserId, PackageId, PurchaseMethod, PurchasedAt, Amount,
OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
VALUES
(
@UserId, @PackageId, 3,
@PurchasedAt, @Amount,
NULL, @TxId, @PurchasedAt, NULL, @PurchasedAt, NULL, 0
);
SET @CreatedUpp = @CreatedUpp + 1;
END
FETCH NEXT FROM curUpp INTO @UserId, @TxId, @PackageId, @Amount, @PurchasedAt;
END
CLOSE curUpp;
DEALLOCATE curUpp;
DROP TABLE #ManualyUppPlan;
PRINT CONCAT('UPP created: ', @CreatedUpp, ' | Skipped (no manualy Tx): ', @SkippedNoTx);
------------------------------------------------------------
-- 8) Missing WH for resolved manualy Tx (history-only, no wallet charge)
------------------------------------------------------------
;WITH TxCandidates AS (
SELECT mu.UserId, t.Id AS TxId, 1 AS Pri
FROM @ManualyUsers mu
INNER JOIN CMS.UserWallets w ON w.UserId = mu.UserId AND w.IsDeleted = 0
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
INNER JOIN CMS.Transactions t ON t.Id = h.RefrenceId
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
UNION ALL
SELECT mu.UserId, t.Id, 2
FROM @ManualyUsers mu
INNER JOIN CMS.Users u ON u.Id = mu.UserId AND u.IsDeleted = 0
INNER JOIN CMS.Transactions t ON t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
AND u.Mobile IN (
NULLIF(LTRIM(RTRIM(t.CreatedBy)), N''),
NULLIF(LTRIM(RTRIM(t.LastModifiedBy)), N'')
)
),
BestTx AS (
SELECT UserId, TxId,
ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY Pri, TxId DESC) AS rn
FROM TxCandidates
)
SELECT
bt.UserId,
bt.TxId,
w.Id AS WalletId,
COALESCE(t.Amount, @BasePackagePrice) AS Amount,
COALESCE(t.PaymentDate, @Now) AS HistoryAt,
COALESCE(
NULLIF(cm.LastPackageId, 0),
NULLIF(cm.FirstPackageId, 0),
@BasePackageId
) AS PackageId,
CAST(ROUND(COALESCE(t.Amount, @BasePackagePrice) * COALESCE(p.DiscountMultiplier, 2.0), 0) AS BIGINT) AS DiscountAmount,
COALESCE(w.NetworkBalance, 0) AS NetworkBalance
INTO #WhToCreate
FROM BestTx bt
INNER JOIN CMS.Transactions t ON t.Id = bt.TxId AND t.IsDeleted = 0
LEFT JOIN CMS.UserWallets w ON w.UserId = bt.UserId AND w.IsDeleted = 0
LEFT JOIN CMS.ClubMemberships cm ON cm.UserId = bt.UserId AND cm.IsDeleted = 0
LEFT JOIN CMS.Packages p ON p.Id = COALESCE(
NULLIF(cm.LastPackageId, 0),
NULLIF(cm.FirstPackageId, 0),
@BasePackageId
) AND p.IsDeleted = 0
WHERE bt.rn = 1
AND NOT EXISTS (
SELECT 1
FROM CMS.UserWalletHistories h
INNER JOIN CMS.UserWallets wh ON wh.Id = h.WalletId AND wh.IsDeleted = 0
WHERE wh.UserId = bt.UserId
AND h.IsDeleted = 0
AND h.RefrenceId = bt.TxId
);
DECLARE @WhPlanCount INT = (SELECT COUNT(*) FROM #WhToCreate);
PRINT CONCAT('WalletHistory to create (no charge): ', @WhPlanCount);
SELECT UserId, TxId, WalletId, Amount, HistoryAt, N'CREATE_HISTORY_ONLY' AS PlanAction
FROM #WhToCreate
ORDER BY UserId;
DECLARE @WalletId BIGINT;
DECLARE @DiscountAmount BIGINT;
DECLARE @HistoryAt DATETIME2;
DECLARE @NetworkBalance BIGINT;
DECLARE @CreatedWh INT = 0;
DECLARE curWh CURSOR LOCAL FAST_FORWARD FOR
SELECT UserId, TxId, WalletId, Amount, PackageId, DiscountAmount, HistoryAt, NetworkBalance
FROM #WhToCreate;
OPEN curWh;
FETCH NEXT FROM curWh INTO
@UserId, @TxId, @WalletId, @Amount, @PackageId, @DiscountAmount, @HistoryAt, @NetworkBalance;
WHILE @@FETCH_STATUS = 0
BEGIN
IF @WalletId IS NULL
BEGIN
INSERT INTO CMS.UserWallets
(UserId, Balance, NetworkBalance, DiscountBalance, WalletMode, Created, LastModified, IsDeleted)
VALUES (@UserId, 0, 0, 0, 0, @HistoryAt, @HistoryAt, 0);
SET @WalletId = SCOPE_IDENTITY();
SET @NetworkBalance = 0;
END
INSERT INTO CMS.UserWalletHistories
(
WalletId, CurrentBalance, ChangeValue, CurrentNetworkBalance, ChangeNerworkValue,
CurrentDiscountBalance, ChangeDiscountValue, IsIncrease, RefrenceId, PackageId,
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
VALUES
(
@WalletId, 0, @Amount, @NetworkBalance, 0,
0, @DiscountAmount, 1, @TxId, @PackageId,
@HistoryAt, NULL, @HistoryAt, NULL, 0
);
SET @CreatedWh = @CreatedWh + 1;
FETCH NEXT FROM curWh INTO
@UserId, @TxId, @WalletId, @Amount, @PackageId, @DiscountAmount, @HistoryAt, @NetworkBalance;
END
CLOSE curWh;
DEALLOCATE curWh;
DROP TABLE #WhToCreate;
PRINT CONCAT('WalletHistory created (no charge): ', @CreatedWh);
------------------------------------------------------------
-- Verify
------------------------------------------------------------
SELECT 'Users still Daya in scope' AS CheckName, COUNT(*) AS Cnt
FROM CMS.Users u
INNER JOIN @ManualyUsers mu ON mu.UserId = u.Id
WHERE u.IsDeleted = 0 AND u.PackagePurchaseMethod = 1
UNION ALL
SELECT 'Still missing UPP in scope', COUNT(*)
FROM @ManualyUsers mu
WHERE NOT EXISTS (
SELECT 1 FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = mu.UserId AND upp.IsDeleted = 0
)
UNION ALL
SELECT 'Broken WH Ref=0 with valid hint remaining', COUNT(*)
FROM CMS.UserWalletHistories h
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
INNER JOIN @ManualyUsers mu ON mu.UserId = w.UserId
WHERE h.IsDeleted = 0
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0
AND EXISTS (
SELECT 1 FROM CMS.Transactions t
WHERE t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
)
UNION ALL
SELECT 'Active stub Daya contracts in scope', COUNT(*)
FROM CMS.DayaLoanContracts d
INNER JOIN @ManualyUsers mu ON mu.UserId = d.UserId
WHERE d.IsDeleted = 0
AND d.IsProcessed = 0
AND d.TransactionId IS NULL
AND d.ProcessedDate IS NULL;
SELECT u.Id AS UserId, u.Mobile, u.FirstName, u.LastName,
u.PackagePurchaseMethod,
(SELECT COUNT(*) FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = u.Id AND upp.IsDeleted = 0) AS UppCnt,
(SELECT COUNT(*) FROM CMS.UserWalletHistories h
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId
WHERE w.UserId = u.Id AND h.IsDeleted = 0) AS WhCnt
FROM CMS.Users u
INNER JOIN @ManualyUsers mu ON mu.UserId = u.Id
WHERE u.IsDeleted = 0
ORDER BY u.Id;
ROLLBACK;
-- COMMIT;
PRINT 'Done (rolled back by default).';
@@ -1,295 +0,0 @@
/*
PHASE 1 — Create UserPackagePurchases (Manual=3) only.
Scope: 17 users with verified manualy charge — both WalletHistory AND Transaction exist
and WH.RefrenceId already points to the Tx.
Hardcoded mapping (from UserWalletHistories.csv + Transactions.csv):
UserId WH Tx
------ ---- ----
75 102 23
93 103 24
96 104 25
99 105 26
47 106 28
43 107 29
45 108 30
52 122 31
116 138 42
115 139 43
119 140 44
113 141 45
103 142 46
170 292 107
171 293 108
172 294 109
175 295 110
Does NOT:
- change Users / Club / Cycle PurchaseMethod
- create or modify Transactions or WalletHistory
- re-charge wallet
PackagePurchaseMethod on UPP: Manual = 3
Defaults to ROLLBACK. Uncomment COMMIT after reviewing preview + counts.
*/
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRAN;
DECLARE @Now DATETIME2 = SYSUTCDATETIME();
DECLARE @BasePackageId BIGINT;
DECLARE @BasePackagePrice BIGINT;
SELECT TOP (1)
@BasePackageId = Id,
@BasePackagePrice = Price
FROM CMS.Packages
WHERE IsDeleted = 0 AND IsBasePackage = 1
ORDER BY Id;
IF @BasePackageId IS NULL
THROW 50001, 'Base package not found.', 1;
------------------------------------------------------------
-- Expected mapping
------------------------------------------------------------
DECLARE @Phase1 TABLE (
UserId BIGINT NOT NULL,
WalletHistoryId BIGINT NOT NULL,
TransactionId BIGINT NOT NULL,
PRIMARY KEY (UserId)
);
INSERT INTO @Phase1 (UserId, WalletHistoryId, TransactionId) VALUES
(75, 102, 23),
(93, 103, 24),
(96, 104, 25),
(99, 105, 26),
(47, 106, 28),
(43, 107, 29),
(45, 108, 30),
(52, 122, 31),
(116, 138, 42),
(115, 139, 43),
(119, 140, 44),
(113, 141, 45),
(103, 142, 46),
(170, 292, 107),
(171, 293, 108),
(172, 294, 109),
(175, 295, 110);
DECLARE @Phase1Count INT = (SELECT COUNT(*) FROM @Phase1);
PRINT CONCAT('Phase1 expected rows: ', @Phase1Count);
------------------------------------------------------------
-- Validate live DB matches expected mapping
------------------------------------------------------------
SELECT
p.UserId,
u.Mobile,
u.FirstName,
u.LastName,
p.WalletHistoryId AS ExpectedWhId,
p.TransactionId AS ExpectedTxId,
h.Id AS ActualWhId,
h.RefrenceId AS ActualWhRefrenceId,
h.ChangeValue AS WhChangeValue,
t.Id AS ActualTxId,
t.RefId AS TxRefId,
t.Amount AS TxAmount,
t.PaymentStatus,
w.UserId AS WhWalletUserId,
CASE
WHEN u.Id IS NULL THEN N'USER_MISSING'
WHEN h.Id IS NULL THEN N'WH_MISSING'
WHEN t.Id IS NULL THEN N'TX_MISSING'
WHEN h.RefrenceId <> p.TransactionId THEN N'WH_REF_MISMATCH'
WHEN w.UserId <> p.UserId THEN N'WH_WALLET_USER_MISMATCH'
WHEN t.RefId NOT LIKE N'manualy%' THEN N'TX_NOT_MANUALY'
WHEN t.PaymentStatus <> 0 THEN N'TX_NOT_SUCCESS'
ELSE N'OK'
END AS ValidationStatus
FROM @Phase1 p
LEFT JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
LEFT JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
LEFT JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
LEFT JOIN CMS.Transactions t ON t.Id = p.TransactionId AND t.IsDeleted = 0
ORDER BY p.UserId;
DECLARE @BadValidation INT = (
SELECT COUNT(*)
FROM @Phase1 p
LEFT JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
LEFT JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
LEFT JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
LEFT JOIN CMS.Transactions t ON t.Id = p.TransactionId AND t.IsDeleted = 0
WHERE u.Id IS NULL
OR h.Id IS NULL
OR t.Id IS NULL
OR h.RefrenceId <> p.TransactionId
OR w.UserId <> p.UserId
OR t.RefId NOT LIKE N'manualy%'
OR t.PaymentStatus <> 0
);
IF @BadValidation > 0
BEGIN
DECLARE @BadMsg NVARCHAR(200);
SET @BadMsg = CONCAT(
N'Validation failed for ', @BadValidation,
N' row(s). Fix before creating UPP.'
);
ROLLBACK;
RAISERROR(@BadMsg, 16, 1);
RETURN;
END
PRINT 'Validation OK for all 17 rows.';
------------------------------------------------------------
-- Preview: who already has UPP / UPP on this Tx
------------------------------------------------------------
SELECT
p.UserId,
u.Mobile,
p.TransactionId,
CASE WHEN upp_any.Id IS NOT NULL THEN N'YES' ELSE N'NO' END AS HasAnyUpp,
upp_any.Id AS ExistingUppId,
upp_any.PurchaseMethod AS ExistingUppMethod,
upp_any.TransactionId AS ExistingUppTxId,
CASE
WHEN upp_tx.Id IS NOT NULL THEN N'SKIP_UPP_EXISTS_FOR_TX'
WHEN upp_any.Id IS NOT NULL THEN N'SKIP_USER_HAS_OTHER_UPP'
ELSE N'CREATE_UPP'
END AS PlanAction
FROM @Phase1 p
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
OUTER APPLY (
SELECT TOP (1) upp.Id, upp.PurchaseMethod, upp.TransactionId
FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0
ORDER BY upp.Id DESC
) upp_any
OUTER APPLY (
SELECT TOP (1) upp.Id
FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = p.UserId
AND upp.IsDeleted = 0
AND upp.TransactionId = p.TransactionId
ORDER BY upp.Id DESC
) upp_tx
ORDER BY p.UserId;
------------------------------------------------------------
-- Build UPP insert plan
------------------------------------------------------------
SELECT
p.UserId,
p.TransactionId,
COALESCE(
NULLIF(cm.LastPackageId, 0),
NULLIF(cm.FirstPackageId, 0),
@BasePackageId
) AS PackageId,
COALESCE(t.Amount, @BasePackagePrice) AS Amount,
COALESCE(t.PaymentDate, cm.ActivatedAt, @Now) AS PurchasedAt
INTO #UppPlan
FROM @Phase1 p
INNER JOIN CMS.Transactions t ON t.Id = p.TransactionId AND t.IsDeleted = 0
LEFT JOIN CMS.ClubMemberships cm ON cm.UserId = p.UserId AND cm.IsDeleted = 0
WHERE NOT EXISTS (
SELECT 1
FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = p.UserId
AND upp.IsDeleted = 0
AND upp.TransactionId = p.TransactionId
);
DECLARE @ToCreate INT = (SELECT COUNT(*) FROM #UppPlan);
DECLARE @Skipped INT = (SELECT COUNT(*) FROM @Phase1) - @ToCreate;
PRINT CONCAT('UPP to create: ', @ToCreate, ' | Already linked (skip): ', @Skipped);
SELECT
pl.UserId,
u.Mobile,
u.FirstName,
u.LastName,
pl.TransactionId,
pl.PackageId,
pl.Amount,
pl.PurchasedAt,
N'INSERT_UPP_MANUAL_3' AS Action
FROM #UppPlan pl
INNER JOIN CMS.Users u ON u.Id = pl.UserId AND u.IsDeleted = 0
ORDER BY pl.UserId;
------------------------------------------------------------
-- Insert UPP
------------------------------------------------------------
INSERT INTO CMS.UserPackagePurchases
(
UserId, PackageId, PurchaseMethod, PurchasedAt, Amount,
OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
SELECT
pl.UserId,
pl.PackageId,
3, -- Manual
pl.PurchasedAt,
pl.Amount,
NULL,
pl.TransactionId,
@Now,
NULL,
@Now,
NULL,
0
FROM #UppPlan pl
WHERE pl.PackageId IS NOT NULL
AND pl.Amount > 0;
PRINT CONCAT('UPP inserted: ', @@ROWCOUNT);
DROP TABLE #UppPlan;
------------------------------------------------------------
-- Verify
------------------------------------------------------------
SELECT
p.UserId,
u.Mobile,
p.TransactionId,
CASE WHEN upp.Id IS NOT NULL THEN N'OK' ELSE N'MISSING_UPP' END AS UppStatus,
upp.Id AS UppId,
upp.PurchaseMethod,
upp.TransactionId AS UppTxId
FROM @Phase1 p
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
LEFT JOIN CMS.UserPackagePurchases upp
ON upp.UserId = p.UserId
AND upp.IsDeleted = 0
AND upp.TransactionId = p.TransactionId
ORDER BY p.UserId;
SELECT N'Missing UPP after run' AS CheckName, COUNT(*) AS Cnt
FROM @Phase1 p
WHERE NOT EXISTS (
SELECT 1
FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = p.UserId
AND upp.IsDeleted = 0
AND upp.TransactionId = p.TransactionId
);
ROLLBACK;
-- COMMIT;
PRINT 'Done (rolled back by default). Uncomment COMMIT when preview looks correct.';
@@ -1,368 +0,0 @@
/*
PHASE 2 — Fix broken WH.RefrenceId + create UPP for 5 remaining manualy txs.
Tx exists; WH either RefrenceId=0 (CreatedBy = Tx Id hint) or missing (Tx 68).
Hardcoded mapping (UserWalletHistories2.csv + Transactions.csv):
UserId WH Tx Notes
------ ----- --- -----
71 118 56 WH Ref=0, CreatedBy=56
72 124 57 WH Ref=0, CreatedBy=57
69 119 58 WH Ref=0, CreatedBy=58
74 120 59 WH Ref=0, CreatedBy=59
142 — 68 no WH — insert history-only (no wallet charge)
Steps:
1) Validate WH hint + Tx manualy% + user/wallet
2) UPDATE WH.RefrenceId for rows 118,124,119,120
3) INSERT UserWalletHistory for User 142 / Tx 68 if missing
4) INSERT UserPackagePurchases Manual(3) for all 5 txs
Does NOT change Users.PackagePurchaseMethod or wallet Balance.
Defaults to ROLLBACK. Uncomment COMMIT after preview.
*/
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRAN;
DECLARE @Now DATETIME2 = SYSUTCDATETIME();
DECLARE @BasePackageId BIGINT;
DECLARE @BasePackagePrice BIGINT;
SELECT TOP (1)
@BasePackageId = Id,
@BasePackagePrice = Price
FROM CMS.Packages
WHERE IsDeleted = 0 AND IsBasePackage = 1
ORDER BY Id;
IF @BasePackageId IS NULL
THROW 50001, 'Base package not found.', 1;
DECLARE @Phase2 TABLE (
UserId BIGINT NOT NULL,
WalletHistoryId BIGINT NULL,
TransactionId BIGINT NOT NULL,
PRIMARY KEY (TransactionId)
);
INSERT INTO @Phase2 (UserId, WalletHistoryId, TransactionId) VALUES
(71, 118, 56),
(72, 124, 57),
(69, 119, 58),
(74, 120, 59),
(142, NULL, 68);
DECLARE @Phase2Count INT = (SELECT COUNT(*) FROM @Phase2);
PRINT CONCAT('Phase2 expected rows: ', @Phase2Count);
------------------------------------------------------------
-- 1) Validate before changes
------------------------------------------------------------
SELECT
p.UserId,
u.Mobile AS UserMobile,
u.FirstName,
u.LastName,
p.WalletHistoryId AS ExpectedWhId,
p.TransactionId AS ExpectedTxId,
h.Id AS ActualWhId,
h.RefrenceId AS WhRefrenceId,
h.CreatedBy AS WhCreatedBy,
h.ChangeValue AS WhChangeValue,
w.UserId AS WhWalletUserId,
t.Id AS ActualTxId,
t.RefId AS TxRefId,
t.Amount AS TxAmount,
t.CreatedBy AS TxCreatedBy,
t.LastModifiedBy AS TxLastModifiedBy,
CASE
WHEN u.Id IS NULL THEN N'USER_MISSING'
WHEN t.Id IS NULL THEN N'TX_MISSING'
WHEN t.RefId NOT LIKE N'manualy%' THEN N'TX_NOT_MANUALY'
WHEN t.PaymentStatus <> 0 THEN N'TX_NOT_SUCCESS'
WHEN p.WalletHistoryId IS NULL THEN N'NO_WH_EXPECTED_CREATE'
WHEN h.Id IS NULL THEN N'WH_MISSING'
WHEN w.UserId <> p.UserId THEN N'WH_WALLET_USER_MISMATCH'
WHEN TRY_CAST(h.CreatedBy AS BIGINT) <> p.TransactionId THEN N'WH_CREATEDBY_HINT_MISMATCH'
WHEN h.RefrenceId = p.TransactionId THEN N'WH_ALREADY_LINKED'
WHEN h.RefrenceId IS NOT NULL AND h.RefrenceId <> 0 AND h.RefrenceId <> p.TransactionId
THEN N'WH_REF_POINTS_OTHER_TX'
ELSE N'OK_FIX_REF'
END AS WhValidation,
CASE
WHEN u.Mobile IN (
NULLIF(LTRIM(RTRIM(t.CreatedBy)), N''),
NULLIF(LTRIM(RTRIM(t.LastModifiedBy)), N'')
) THEN N'MOBILE_OK'
WHEN NULLIF(LTRIM(RTRIM(t.CreatedBy)), N'') LIKE N'09%' THEN N'TX_MOBILE_USER_MISMATCH'
ELSE N'TX_NO_MOBILE_ON_ROW'
END AS MobileCheck
FROM @Phase2 p
LEFT JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
LEFT JOIN CMS.Transactions t ON t.Id = p.TransactionId AND t.IsDeleted = 0
LEFT JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
LEFT JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
ORDER BY p.TransactionId;
DECLARE @BadWh INT = (
SELECT COUNT(*)
FROM @Phase2 p
LEFT JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
LEFT JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
LEFT JOIN CMS.Transactions t ON t.Id = p.TransactionId AND t.IsDeleted = 0
LEFT JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
WHERE t.Id IS NULL
OR u.Id IS NULL
OR t.RefId NOT LIKE N'manualy%'
OR t.PaymentStatus <> 0
OR (
p.WalletHistoryId IS NOT NULL
AND (
h.Id IS NULL
OR w.UserId <> p.UserId
OR TRY_CAST(h.CreatedBy AS BIGINT) <> p.TransactionId
OR (h.RefrenceId IS NOT NULL AND h.RefrenceId <> 0 AND h.RefrenceId <> p.TransactionId)
)
)
);
IF @BadWh > 0
BEGIN
DECLARE @BadMsg NVARCHAR(200);
SET @BadMsg = CONCAT(N'Validation failed for ', @BadWh, N' row(s). Aborting.');
ROLLBACK;
RAISERROR(@BadMsg, 16, 1);
RETURN;
END
PRINT 'Validation OK.';
------------------------------------------------------------
-- 2) Fix WH.RefrenceId (118,124,119,120)
------------------------------------------------------------
SELECT
p.UserId,
p.WalletHistoryId,
p.TransactionId,
h.RefrenceId AS OldRefrenceId,
N'UPDATE RefrenceId' AS Action
FROM @Phase2 p
INNER JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
WHERE p.WalletHistoryId IS NOT NULL
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0 OR h.RefrenceId <> p.TransactionId);
UPDATE h
SET
h.RefrenceId = p.TransactionId,
h.LastModified = @Now
FROM @Phase2 p
INNER JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
WHERE p.WalletHistoryId IS NOT NULL
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0);
PRINT CONCAT('WalletHistory RefrenceId updated: ', @@ROWCOUNT);
------------------------------------------------------------
-- 3) User 142 / Tx 68 — history-only WH if missing
------------------------------------------------------------
SELECT
p.UserId,
p.TransactionId,
w.Id AS WalletId,
h.Id AS ExistingWhId,
CASE WHEN h.Id IS NOT NULL THEN N'SKIP_WH_EXISTS' ELSE N'CREATE_WH_HISTORY' END AS WhPlan
FROM @Phase2 p
LEFT JOIN CMS.UserWallets w ON w.UserId = p.UserId AND w.IsDeleted = 0
LEFT JOIN CMS.UserWalletHistories h ON h.IsDeleted = 0 AND h.RefrenceId = p.TransactionId
AND h.WalletId = w.Id
WHERE p.WalletHistoryId IS NULL;
DECLARE @UserId BIGINT = 142;
DECLARE @TxId BIGINT = 68;
DECLARE @WalletId BIGINT;
DECLARE @PackageId BIGINT;
DECLARE @Amount BIGINT;
DECLARE @HistoryAt DATETIME2;
DECLARE @DiscountAmount BIGINT;
DECLARE @NetworkBalance BIGINT;
IF EXISTS (SELECT 1 FROM @Phase2 WHERE TransactionId = 68 AND WalletHistoryId IS NULL)
BEGIN
SELECT
@WalletId = w.Id,
@PackageId = COALESCE(
NULLIF(cm.LastPackageId, 0),
NULLIF(cm.FirstPackageId, 0),
@BasePackageId
),
@Amount = t.Amount,
@HistoryAt = COALESCE(t.PaymentDate, @Now),
@NetworkBalance = COALESCE(w.NetworkBalance, 0),
@DiscountAmount = CAST(ROUND(t.Amount * COALESCE(pkg.DiscountMultiplier, 2.0), 0) AS BIGINT)
FROM CMS.Transactions t
INNER JOIN CMS.Users u ON u.Id = 142 AND u.IsDeleted = 0
LEFT JOIN CMS.UserWallets w ON w.UserId = u.Id AND w.IsDeleted = 0
LEFT JOIN CMS.ClubMemberships cm ON cm.UserId = u.Id AND cm.IsDeleted = 0
LEFT JOIN CMS.Packages pkg ON pkg.Id = COALESCE(
NULLIF(cm.LastPackageId, 0),
NULLIF(cm.FirstPackageId, 0),
@BasePackageId
) AND pkg.IsDeleted = 0
WHERE t.Id = 68 AND t.IsDeleted = 0;
IF @WalletId IS NULL
BEGIN
INSERT INTO CMS.UserWallets
(UserId, Balance, NetworkBalance, DiscountBalance, WalletMode, Created, LastModified, IsDeleted)
VALUES (142, 0, 0, 0, 0, @HistoryAt, @HistoryAt, 0);
SET @WalletId = SCOPE_IDENTITY();
SET @NetworkBalance = 0;
PRINT CONCAT('Wallet created for User 142: ', @WalletId);
END
IF NOT EXISTS (
SELECT 1
FROM CMS.UserWalletHistories h
WHERE h.WalletId = @WalletId
AND h.IsDeleted = 0
AND h.RefrenceId = 68
)
BEGIN
INSERT INTO CMS.UserWalletHistories
(
WalletId, CurrentBalance, ChangeValue, CurrentNetworkBalance, ChangeNerworkValue,
CurrentDiscountBalance, ChangeDiscountValue, IsIncrease, RefrenceId, PackageId,
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
VALUES
(
@WalletId, 0, @Amount, @NetworkBalance, 0,
0, @DiscountAmount, 1, 68, @PackageId,
@HistoryAt, NULL, @HistoryAt, NULL, 0
);
PRINT CONCAT('WalletHistory created for Tx 68 (no charge): WH Id=', SCOPE_IDENTITY());
END
ELSE
PRINT 'WalletHistory for Tx 68 already exists — skipped.';
END
------------------------------------------------------------
-- 4) UPP preview + insert
------------------------------------------------------------
SELECT
p.UserId,
u.Mobile,
p.TransactionId,
CASE WHEN upp.Id IS NOT NULL THEN N'SKIP_UPP_EXISTS' ELSE N'CREATE_UPP' END AS UppPlan,
upp.Id AS ExistingUppId
FROM @Phase2 p
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
LEFT JOIN CMS.UserPackagePurchases upp
ON upp.UserId = p.UserId
AND upp.IsDeleted = 0
AND upp.TransactionId = p.TransactionId
ORDER BY p.TransactionId;
SELECT
p.UserId,
p.TransactionId,
COALESCE(
NULLIF(cm.LastPackageId, 0),
NULLIF(cm.FirstPackageId, 0),
@BasePackageId
) AS PackageId,
COALESCE(t.Amount, @BasePackagePrice) AS Amount,
COALESCE(t.PaymentDate, cm.ActivatedAt, @Now) AS PurchasedAt
INTO #UppPlan
FROM @Phase2 p
INNER JOIN CMS.Transactions t ON t.Id = p.TransactionId AND t.IsDeleted = 0
LEFT JOIN CMS.ClubMemberships cm ON cm.UserId = p.UserId AND cm.IsDeleted = 0
WHERE NOT EXISTS (
SELECT 1
FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = p.UserId
AND upp.IsDeleted = 0
AND upp.TransactionId = p.TransactionId
);
DECLARE @UppToCreate INT = (SELECT COUNT(*) FROM #UppPlan);
PRINT CONCAT('UPP to create: ', @UppToCreate);
INSERT INTO CMS.UserPackagePurchases
(
UserId, PackageId, PurchaseMethod, PurchasedAt, Amount,
OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
SELECT
pl.UserId,
pl.PackageId,
3,
pl.PurchasedAt,
pl.Amount,
NULL,
pl.TransactionId,
@Now,
NULL,
@Now,
NULL,
0
FROM #UppPlan pl
WHERE pl.PackageId IS NOT NULL AND pl.Amount > 0;
PRINT CONCAT('UPP inserted: ', @@ROWCOUNT);
DROP TABLE #UppPlan;
------------------------------------------------------------
-- 5) Verify
------------------------------------------------------------
SELECT
p.UserId,
u.Mobile,
p.TransactionId,
h.Id AS WalletHistoryId,
h.RefrenceId AS WhRefrenceId,
upp.Id AS UppId,
upp.PurchaseMethod,
CASE
WHEN h.Id IS NULL AND p.WalletHistoryId IS NOT NULL THEN N'MISSING_WH'
WHEN h.RefrenceId <> p.TransactionId AND p.WalletHistoryId IS NOT NULL THEN N'WH_REF_WRONG'
WHEN NOT EXISTS (
SELECT 1 FROM CMS.UserWalletHistories hx
INNER JOIN CMS.UserWallets wx ON wx.Id = hx.WalletId AND wx.UserId = p.UserId
WHERE hx.IsDeleted = 0 AND hx.RefrenceId = p.TransactionId
) THEN N'NO_WH_FOR_TX'
WHEN upp.Id IS NULL THEN N'MISSING_UPP'
ELSE N'OK'
END AS FinalStatus
FROM @Phase2 p
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
LEFT JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
LEFT JOIN CMS.UserPackagePurchases upp
ON upp.UserId = p.UserId
AND upp.IsDeleted = 0
AND upp.TransactionId = p.TransactionId
ORDER BY p.TransactionId;
SELECT N'Incomplete after run' AS CheckName, COUNT(*) AS Cnt
FROM @Phase2 p
WHERE NOT EXISTS (
SELECT 1 FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0 AND upp.TransactionId = p.TransactionId
)
OR NOT EXISTS (
SELECT 1
FROM CMS.UserWalletHistories h
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.UserId = p.UserId
WHERE h.IsDeleted = 0 AND h.RefrenceId = p.TransactionId
);
ROLLBACK;
-- COMMIT;
PRINT 'Done (rolled back by default). Uncomment COMMIT when preview looks correct.';
@@ -1,355 +0,0 @@
/*
PHASE 3 — Remaining 16 users from WrongDaya→Manual list (no manualy Tx from Phase 1/2).
Users:
2,7,8,9,10,11,12,13,40,41,105,106,130,162,169,176
Already done in Phase 1+2 (22 users with manualy Tx): excluded here.
Per user (skip if UPP already exists for that UserId):
1) Reuse deposit Tx if WH links to Success Tx with Amount=56000000
2) Else fix WH.RefrenceId when CreatedBy hints an existing manualy Tx
3) Else CREATE Transaction (RefId FIX3-{UserId}) — no wallet charge
4) Link stub WH (Ref=0) to Tx OR insert history-only WH
5) INSERT UserPackagePurchases Manual(3)
User 162: skipped for UPP if any UPP already exists (has UPP from ManualPayment flow).
Does NOT change Users.PackagePurchaseMethod (Phase 4 later).
Defaults to ROLLBACK. Uncomment COMMIT after preview.
*/
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRAN;
DECLARE @Now DATETIME2 = SYSUTCDATETIME();
DECLARE @ChargeAmount BIGINT = 56000000;
DECLARE @BasePackageId BIGINT;
DECLARE @BasePackagePrice BIGINT;
SELECT TOP (1)
@BasePackageId = Id,
@BasePackagePrice = Price
FROM CMS.Packages
WHERE IsDeleted = 0 AND IsBasePackage = 1
ORDER BY Id;
IF @BasePackageId IS NULL
THROW 50001, 'Base package not found.', 1;
DECLARE @Phase3 TABLE (UserId BIGINT PRIMARY KEY);
INSERT INTO @Phase3 (UserId) VALUES
(2),(7),(8),(9),(10),(11),(12),(13),(40),(41),
(105),(106),(130),(162),(169),(176);
DECLARE @Phase3Count INT = (SELECT COUNT(*) FROM @Phase3);
PRINT CONCAT('Phase3 users: ', @Phase3Count);
------------------------------------------------------------
-- Preview: current ledger state
------------------------------------------------------------
SELECT
p.UserId,
u.Mobile,
u.FirstName,
u.LastName,
u.PackagePurchaseMethod,
(SELECT COUNT(*) FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0) AS UppCount,
(SELECT TOP (1) upp.Id FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0 ORDER BY upp.Id DESC) AS LatestUppId,
(SELECT COUNT(*) FROM CMS.UserWalletHistories h
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
WHERE w.UserId = p.UserId AND h.IsDeleted = 0) AS WhCount,
(SELECT COUNT(*) FROM CMS.UserWalletHistories h
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
WHERE w.UserId = p.UserId AND h.IsDeleted = 0
AND h.ChangeValue >= @ChargeAmount AND h.RefrenceId > 0) AS WhFullDepositCount,
CASE
WHEN EXISTS (
SELECT 1 FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0
) THEN N'SKIP_HAS_UPP'
ELSE N'NEEDS_UPP'
END AS UppPlan
FROM @Phase3 p
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
ORDER BY p.UserId;
------------------------------------------------------------
-- Resolve Tx per user needing UPP
------------------------------------------------------------
SELECT
p.UserId,
u.Mobile,
COALESCE(
NULLIF(cm.LastPackageId, 0),
NULLIF(cm.FirstPackageId, 0),
@BasePackageId
) AS PackageId,
COALESCE(cm.ActivatedAt, cm.Created, @Now) AS PurchasedAt,
dep.TxId AS ExistingDepositTxId,
dep.WhId AS ExistingDepositWhId,
stub.StubWhId,
stub.StubCreatedBy,
CAST(NULL AS BIGINT) AS ResolvedTxId,
CAST(N'' AS NVARCHAR(40)) AS TxSource,
CAST(N'' AS NVARCHAR(40)) AS PlanAction
INTO #Plan
FROM @Phase3 p
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
LEFT JOIN CMS.ClubMemberships cm ON cm.UserId = p.UserId AND cm.IsDeleted = 0
OUTER APPLY (
SELECT TOP (1)
t.Id AS TxId,
h.Id AS WhId
FROM CMS.UserWallets w
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
INNER JOIN CMS.Transactions t ON t.Id = h.RefrenceId
AND t.IsDeleted = 0 AND t.PaymentStatus = 0 AND t.Amount = @ChargeAmount
WHERE w.UserId = p.UserId AND w.IsDeleted = 0
AND h.ChangeValue >= @ChargeAmount
AND h.IsIncrease = 1
ORDER BY h.Id ASC
) dep
OUTER APPLY (
SELECT TOP (1)
h.Id AS StubWhId,
h.CreatedBy AS StubCreatedBy
FROM CMS.UserWallets w
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
WHERE w.UserId = p.UserId AND w.IsDeleted = 0
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
ORDER BY h.Id ASC
) stub
WHERE NOT EXISTS (
SELECT 1 FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0
);
-- Pass 1: existing full deposit WH→Tx
UPDATE pl
SET
ResolvedTxId = pl.ExistingDepositTxId,
TxSource = N'WH_DEPOSIT',
PlanAction = N'CREATE_UPP_REUSE_TX'
FROM #Plan pl
WHERE pl.ExistingDepositTxId IS NOT NULL;
-- Pass 2: stub CreatedBy → manualy Tx
UPDATE pl
SET
ResolvedTxId = t.Id,
TxSource = N'WH_STUB_HINT',
PlanAction = N'FIX_STUB_LINK_UPP'
FROM #Plan pl
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(pl.StubCreatedBy AS BIGINT)
AND t.IsDeleted = 0 AND t.PaymentStatus = 0 AND t.RefId LIKE N'manualy%'
WHERE pl.ResolvedTxId IS NULL
AND TRY_CAST(pl.StubCreatedBy AS BIGINT) > 0;
-- Pass 3: need new Tx
UPDATE pl
SET
TxSource = N'CREATE_TX',
PlanAction = N'CREATE_TX_WH_UPP'
FROM #Plan pl
WHERE pl.ResolvedTxId IS NULL;
SELECT
UserId, Mobile, PackageId, PurchasedAt,
ExistingDepositTxId, ExistingDepositWhId,
StubWhId, StubCreatedBy,
ResolvedTxId, TxSource, PlanAction
FROM #Plan
ORDER BY UserId;
DECLARE @NeedPlan INT = (SELECT COUNT(*) FROM #Plan);
PRINT CONCAT('Users needing UPP in Phase3: ', @NeedPlan);
------------------------------------------------------------
-- Execute: cursor per user
------------------------------------------------------------
DECLARE @UserId BIGINT;
DECLARE @Mobile NVARCHAR(50);
DECLARE @PackageId BIGINT;
DECLARE @PurchasedAt DATETIME2;
DECLARE @ExistingDepositTxId BIGINT;
DECLARE @StubWhId BIGINT;
DECLARE @ResolvedTxId BIGINT;
DECLARE @TxSource NVARCHAR(40);
DECLARE @TxId BIGINT;
DECLARE @WalletId BIGINT;
DECLARE @DiscountAmount BIGINT;
DECLARE @NetworkBalance BIGINT;
DECLARE @HistoryAt DATETIME2;
DECLARE @CreatedTx INT = 0;
DECLARE @CreatedUpp INT = 0;
DECLARE @CreatedWh INT = 0;
DECLARE @FixedWh INT = 0;
DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
SELECT UserId, Mobile, PackageId, PurchasedAt,
ExistingDepositTxId, StubWhId, ResolvedTxId, TxSource
FROM #Plan;
OPEN cur;
FETCH NEXT FROM cur INTO
@UserId, @Mobile, @PackageId, @PurchasedAt,
@ExistingDepositTxId, @StubWhId, @ResolvedTxId, @TxSource;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @TxId = @ResolvedTxId;
IF @TxId IS NULL
BEGIN
INSERT INTO CMS.Transactions
(
Amount, Description, PaymentStatus, PaymentDate, RefId, Type,
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
VALUES
(
@ChargeAmount,
N'عضویت دستی باشگاه مشتریان - backfill Phase3 WrongDaya',
0,
@PurchasedAt,
CONCAT(N'FIX3-', @UserId),
2,
@PurchasedAt,
@Mobile,
@PurchasedAt,
@Mobile,
0
);
SET @TxId = SCOPE_IDENTITY();
SET @CreatedTx = @CreatedTx + 1;
END
IF @StubWhId IS NOT NULL
AND EXISTS (
SELECT 1 FROM CMS.UserWalletHistories h
WHERE h.Id = @StubWhId AND h.IsDeleted = 0
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
)
BEGIN
UPDATE CMS.UserWalletHistories
SET RefrenceId = @TxId, LastModified = @Now
WHERE Id = @StubWhId;
SET @FixedWh = @FixedWh + @@ROWCOUNT;
END
SELECT @WalletId = w.Id, @NetworkBalance = COALESCE(w.NetworkBalance, 0)
FROM CMS.UserWallets w
WHERE w.UserId = @UserId AND w.IsDeleted = 0;
IF @WalletId IS NULL
BEGIN
INSERT INTO CMS.UserWallets
(UserId, Balance, NetworkBalance, DiscountBalance, WalletMode, Created, LastModified, IsDeleted)
VALUES (@UserId, 0, 0, 0, 0, @PurchasedAt, @PurchasedAt, 0);
SET @WalletId = SCOPE_IDENTITY();
SET @NetworkBalance = 0;
END
IF NOT EXISTS (
SELECT 1 FROM CMS.UserWalletHistories h
WHERE h.WalletId = @WalletId AND h.IsDeleted = 0 AND h.RefrenceId = @TxId
)
BEGIN
SELECT
@HistoryAt = COALESCE(t.PaymentDate, @PurchasedAt),
@DiscountAmount = CAST(ROUND(@ChargeAmount * COALESCE(pkg.DiscountMultiplier, 2.0), 0) AS BIGINT)
FROM CMS.Transactions t
LEFT JOIN CMS.Packages pkg ON pkg.Id = @PackageId AND pkg.IsDeleted = 0
WHERE t.Id = @TxId;
INSERT INTO CMS.UserWalletHistories
(
WalletId, CurrentBalance, ChangeValue, CurrentNetworkBalance, ChangeNerworkValue,
CurrentDiscountBalance, ChangeDiscountValue, IsIncrease, RefrenceId, PackageId,
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
VALUES
(
@WalletId, 0, @ChargeAmount, @NetworkBalance, 0,
0, @DiscountAmount, 1, @TxId, @PackageId,
@HistoryAt, NULL, @HistoryAt, NULL, 0
);
SET @CreatedWh = @CreatedWh + 1;
END
IF NOT EXISTS (
SELECT 1 FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = @UserId AND upp.IsDeleted = 0 AND upp.TransactionId = @TxId
)
BEGIN
INSERT INTO CMS.UserPackagePurchases
(
UserId, PackageId, PurchaseMethod, PurchasedAt, Amount,
OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
VALUES
(
@UserId, @PackageId, 3,
@PurchasedAt, @ChargeAmount,
NULL, @TxId, @Now, NULL, @Now, NULL, 0
);
SET @CreatedUpp = @CreatedUpp + 1;
END
FETCH NEXT FROM cur INTO
@UserId, @Mobile, @PackageId, @PurchasedAt,
@ExistingDepositTxId, @StubWhId, @ResolvedTxId, @TxSource;
END
CLOSE cur;
DEALLOCATE cur;
DROP TABLE #Plan;
PRINT CONCAT('Tx created: ', @CreatedTx,
' | WH Ref fixed: ', @FixedWh,
' | WH created: ', @CreatedWh,
' | UPP created: ', @CreatedUpp);
------------------------------------------------------------
-- Verify all 16 have UPP
------------------------------------------------------------
SELECT
p.UserId,
u.Mobile,
CASE WHEN upp.UppId IS NOT NULL THEN N'OK' ELSE N'MISSING_UPP' END AS UppStatus,
upp.UppId,
upp.PurchaseMethod,
upp.TransactionId AS UppTxId,
t.RefId AS TxRefId
FROM @Phase3 p
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
OUTER APPLY (
SELECT TOP (1)
upp2.Id AS UppId,
upp2.PurchaseMethod,
upp2.TransactionId
FROM CMS.UserPackagePurchases upp2
WHERE upp2.UserId = p.UserId AND upp2.IsDeleted = 0
ORDER BY upp2.Id DESC
) upp
LEFT JOIN CMS.Transactions t ON t.Id = upp.TransactionId
ORDER BY p.UserId;
SELECT N'Phase3 users still without UPP' AS CheckName, COUNT(*) AS Cnt
FROM @Phase3 p
WHERE NOT EXISTS (
SELECT 1 FROM CMS.UserPackagePurchases upp
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0
);
ROLLBACK;
-- COMMIT;
PRINT 'Done (rolled back by default). Uncomment COMMIT when preview looks correct.';

Some files were not shown because too many files have changed in this diff Show More