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
51 changed files with 224 additions and 2000 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
+5 -3
View File
@@ -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
@@ -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: |
+1 -1
View File
@@ -1,4 +1,4 @@
# CMS Microservice
Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
@@ -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;
}
@@ -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,4 +1,5 @@
using System.Data;
using System.Data.Common;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
@@ -41,11 +42,6 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
// تبدیل نتایج flat به ساختار درختی
var tree = BuildTreeFromFlatNodes(flatNodes);
if (tree != null)
{
await EnrichWithPackageLegScoresAsync(tree, flatNodes, cancellationToken);
}
return tree;
}
catch (Exception ex)
@@ -198,93 +194,4 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
var rootNode = flatNodes.FirstOrDefault(n => n.NetworkLevel == 0);
return rootNode != null ? nodeDict[rootNode.UserId] : null;
}
/// <summary>
/// آخرین Left/RightLegTotal طلایی و نقره‌ای برای هر نود درخت (برنامه‌ریزی پای چپ/راست).
/// </summary>
private async Task EnrichWithPackageLegScoresAsync(
NetworkTreeDto root,
List<NetworkTreeNodeDto> flatNodes,
CancellationToken cancellationToken)
{
var userIds = flatNodes.Select(n => n.UserId).Distinct().ToList();
if (userIds.Count == 0) return;
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;
if (!goldPackageId.HasValue && !silverPackageId.HasValue)
{
_logger.LogWarning("GetNetworkTree: no gold/silver packages found for leg score enrichment");
return;
}
var packageIds = new List<long>();
if (goldPackageId.HasValue) packageIds.Add(goldPackageId.Value);
if (silverPackageId.HasValue && silverPackageId != goldPackageId)
packageIds.Add(silverPackageId.Value);
var rawBalances = await _context.NetworkWeeklyBalances
.AsNoTracking()
.Where(b => userIds.Contains(b.UserId) && packageIds.Contains(b.PackageId))
.Select(b => new LegScoreRow(
b.UserId,
b.PackageId,
b.WeekDefinitionId,
b.LeftLegTotal,
b.RightLegTotal))
.ToListAsync(cancellationToken);
var latestByUserPackage = rawBalances
.GroupBy(b => (b.UserId, b.PackageId))
.ToDictionary(
g => g.Key,
g => g.OrderByDescending(x => x.WeekDefinitionId).First());
ApplyLegScores(root, goldPackageId, silverPackageId, latestByUserPackage);
}
private static void ApplyLegScores(
NetworkTreeDto node,
long? goldPackageId,
long? silverPackageId,
Dictionary<(long UserId, long PackageId), LegScoreRow> latestByUserPackage)
{
if (goldPackageId.HasValue &&
latestByUserPackage.TryGetValue((node.UserId, goldPackageId.Value), out var gold))
{
node.GoldLeftLegTotal = gold.LeftLegTotal;
node.GoldRightLegTotal = gold.RightLegTotal;
}
if (silverPackageId.HasValue &&
latestByUserPackage.TryGetValue((node.UserId, silverPackageId.Value), out var silver))
{
node.SilverLeftLegTotal = silver.LeftLegTotal;
node.SilverRightLegTotal = silver.RightLegTotal;
}
if (node.LeftChild != null)
ApplyLegScores(node.LeftChild, goldPackageId, silverPackageId, latestByUserPackage);
if (node.RightChild != null)
ApplyLegScores(node.RightChild, goldPackageId, silverPackageId, latestByUserPackage);
}
private sealed record LegScoreRow(
long UserId,
long PackageId,
long WeekDefinitionId,
int LeftLegTotal,
int RightLegTotal);
}
@@ -60,18 +60,6 @@ public class NetworkTreeDto
public long? PackageId { get; set; }
/// <summary>آخرین LeftLegTotal پکیج طلایی (پایه)</summary>
public int GoldLeftLegTotal { get; set; }
/// <summary>آخرین RightLegTotal پکیج طلایی (پایه)</summary>
public int GoldRightLegTotal { get; set; }
/// <summary>آخرین LeftLegTotal پکیج نقره‌ای</summary>
public int SilverLeftLegTotal { get; set; }
/// <summary>آخرین RightLegTotal پکیج نقره‌ای</summary>
public int SilverRightLegTotal { get; set; }
public NetworkTreeDto? LeftChild { get; set; }
public NetworkTreeDto? RightChild { get; set; }
}
@@ -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,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
};
}
}
@@ -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
}
@@ -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 />
@@ -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)
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.210</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; // شناسه پکیج
// آخرین NetworkWeeklyBalance: امتیاز پای چپ/راست برای پکیج طلایی و نقره‌ای (D3)
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
@@ -102,10 +102,6 @@ enum DeliveryStatus
DeliveryStatus_Delivered = 3;
// مرجوع شده
DeliveryStatus_Returned = 4;
// لغو شده
DeliveryStatus_Cancelled = 5;
// آماده تحویل حضوری در دفتر
DeliveryStatus_ReadyForOfficePickup = 6;
}
enum TransactionType
{
@@ -238,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
{
@@ -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)
@@ -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}");
}
}
@@ -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;
@@ -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);
}
@@ -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,19 +2,16 @@
"PaymentProvider": "zarinpal",
"ZarinPal": {
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
"UseSandbox": true
},
"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=194.5.195.53,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": {
@@ -23,12 +20,12 @@
},
"Monitoring": {
"SentryEnabled": false,
"SentryDsn": "",
"SentryDsn": "",
"SlackEnabled": false,
"SlackWebhookUrl": "",
"EmailAlertsEnabled": false,
"AdminEmails": [
"admin@example.com"
"admin@example.com"
],
"SmsNotificationsEnabled": false,
"SmsApiKey": "",
@@ -67,7 +64,7 @@
},
"BackgroundJobs": {
"WeeklyCommissionCalculation": {
"Enabled": true,
"Enabled": false,
"CronExpression": "5 0 * * 0"
}
},
@@ -87,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 -17
View File
@@ -2,24 +2,16 @@
"PaymentProvider": "zarinpal",
"ZarinPal": {
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
"UseSandbox": true
},
"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=194.5.195.53,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": {
@@ -27,12 +19,12 @@
},
"Monitoring": {
"SentryEnabled": false,
"SentryDsn": "",
"SentryDsn": "",
"SlackEnabled": false,
"SlackWebhookUrl": "",
"EmailAlertsEnabled": false,
"AdminEmails": [
"admin@example.com"
"admin@example.com"
],
"SmsNotificationsEnabled": false,
"SmsApiKey": "",
@@ -51,7 +43,7 @@
"Sms": {
"Enabled": true,
"Provider": "Kavenegar",
"KavenegarApiKey": "43676A4E786A6C50452F2F6252507939346A5145764B33566B456454374E657A614468706658656D6534593D",
"KavenegarApiKey": "497263626F32626A48685A6137524C4F78575A766E4C74694A556B79317648424964655030682B554545413D",
"Sender": "1000001110100"
},
"DayaPayment": {
@@ -71,7 +63,7 @@
},
"BackgroundJobs": {
"WeeklyCommissionCalculation": {
"Enabled": true,
"Enabled": false,
"CronExpression": "5 0 * * 0"
}
},
@@ -84,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"
}
}
}
@@ -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,465 +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 + charge)
If ExistingDayaTxId found (wallet RefrenceId → Tx Description LIKE '%اعتبار دایا%'
OR Tx.RefId = ContractNumber): REUSE that Tx — do NOT create Transaction / do NOT charge wallet.
Special UserId mapping (shared NationalCode):
C4_D8815963 / 2490371436 → UserId 50
C4_G8945289 / 2490371436 → UserId 51
@Commit = 0 → dry-run (ROLLBACK)
@Commit = 1 → apply
*/
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @Commit BIT = 0; -- << set 1 to apply
DECLARE @Now DATETIME2 = SYSUTCDATETIME();
DECLARE @PackageId BIGINT;
DECLARE @PackagePrice BIGINT;
DECLARE @DiscountMultiplier DECIMAL(18,4);
DECLARE @DiscountAmount BIGINT;
SELECT TOP (1)
@PackageId = Id,
@PackagePrice = Price,
@DiscountMultiplier = DiscountMultiplier
FROM CMS.Packages
WHERE IsDeleted = 0 AND IsBasePackage = 1
ORDER BY Id;
IF @PackageId IS NULL
THROW 50001, 'Base package not found.', 1;
SET @DiscountAmount = CAST(@PackagePrice * @DiscountMultiplier AS BIGINT);
PRINT CONCAT('PackageId=', @PackageId, ' Price=', @PackagePrice, ' Discount=', @DiscountAmount);
------------------------------------------------------------
-- Targets: Excel «فعال شده»
------------------------------------------------------------
DECLARE @Targets TABLE (
ContractNumber NVARCHAR(50) NOT NULL,
NationalCode NVARCHAR(20) NOT NULL,
UserIdHint BIGINT NULL -- explicit override when NC is shared
);
INSERT INTO @Targets (ContractNumber, NationalCode, UserIdHint) VALUES
(N'C4_H10706299', N'4060667961', NULL),
(N'C4_P9320383', N'2572729636', NULL),
(N'C4_Q9465813', N'2480017796', NULL),
(N'C4_B9348378', N'0452314488', NULL),
(N'C4_X9630520', N'0680164286', NULL),
(N'C4_B9813553', N'0055680364', NULL), -- may match DayaLoanContracts.NationalCode if Users.NC differs
(N'C4_M9994735', N'0793851572', NULL),
(N'C4_S9301452', N'0082242925', NULL),
(N'C4_T9961063', N'0312342993', NULL),
(N'C4_L9524515', N'0024757489', NULL),
(N'C4_Q9947535', N'2480259668', NULL),
(N'C4_T9892089', N'2451477016', NULL),
(N'C4_W8785358', N'0322922720', NULL),
(N'C4_D8815963', N'2490371436', 50), -- earlier activation → user 50
(N'C4_G8945289', N'2490371436', 51), -- later activation → user 51
(N'C4_J8388494', N'2002637903', NULL);
DECLARE @Work TABLE (
ContractNumber NVARCHAR(50),
NationalCode NVARCHAR(20),
UserId BIGINT,
DayaContractId BIGINT NULL,
HasCredit BIT,
IsProcessed BIT,
HasDayaUpp BIT,
ExistingDayaTxId BIGINT NULL, -- Tx از قبل با شرح «اعتبار دایا» / RefId قرارداد (شارژ شده)
Verdict NVARCHAR(20)
);
INSERT INTO @Work (ContractNumber, NationalCode, UserId, DayaContractId, HasCredit, IsProcessed, HasDayaUpp, ExistingDayaTxId, Verdict)
SELECT
t.ContractNumber,
t.NationalCode,
u.Id,
d.Id,
CAST(ISNULL(u.HasReceivedDayaCredit, 0) AS BIT),
CAST(ISNULL(d.IsProcessed, 0) AS BIT),
CAST(CASE WHEN EXISTS (
SELECT 1
FROM CMS.UserPackagePurchases upp
LEFT JOIN CMS.Transactions tx ON tx.Id = upp.TransactionId AND tx.IsDeleted = 0
WHERE upp.UserId = u.Id
AND upp.IsDeleted = 0
AND upp.PurchaseMethod = 1
AND (upp.TransactionId IS NULL OR tx.PaymentStatus = 0)
) THEN 1 ELSE 0 END AS BIT),
etx.TxId,
CASE
WHEN u.Id IS NULL THEN N'NO_USER'
WHEN ISNULL(u.HasReceivedDayaCredit, 0) = 1
AND ISNULL(d.IsProcessed, 0) = 1
AND EXISTS (
SELECT 1
FROM CMS.UserPackagePurchases upp
LEFT JOIN CMS.Transactions tx ON tx.Id = upp.TransactionId AND tx.IsDeleted = 0
WHERE upp.UserId = u.Id
AND upp.IsDeleted = 0
AND upp.PurchaseMethod = 1
AND (upp.TransactionId IS NULL OR tx.PaymentStatus = 0)
) THEN N'DONE'
-- قبلاً شارژ دایا در کیف/تراکنش هست ولی فلگ/UPP/قرارداد کامل نیست
WHEN etx.TxId IS NOT NULL THEN N'PARTIAL'
WHEN ISNULL(u.HasReceivedDayaCredit, 0) = 1
OR ISNULL(d.IsProcessed, 0) = 1 THEN N'PARTIAL'
ELSE N'NOT_PROCESSED'
END
FROM @Targets t
OUTER APPLY (
SELECT TOP (1) u0.*
FROM CMS.Users u0
WHERE u0.IsDeleted = 0
AND (
(t.UserIdHint IS NOT NULL AND u0.Id = t.UserIdHint)
OR (t.UserIdHint IS NULL AND u0.NationalCode = t.NationalCode)
OR (t.UserIdHint IS NULL AND EXISTS (
SELECT 1 FROM CMS.DayaLoanContracts dx
WHERE dx.UserId = u0.Id
AND dx.NationalCode = t.NationalCode
AND dx.IsDeleted = 0
))
)
ORDER BY
CASE WHEN t.UserIdHint IS NOT NULL AND u0.Id = t.UserIdHint THEN 0 ELSE 1 END,
CASE WHEN u0.NationalCode = t.NationalCode THEN 0 ELSE 1 END,
u0.Id
) u
OUTER APPLY (
SELECT TOP (1) d0.*
FROM CMS.DayaLoanContracts d0
WHERE d0.UserId = u.Id
AND (
d0.ContractNumber = t.ContractNumber
OR d0.NationalCode = t.NationalCode
OR d0.IsDeleted IN (0, 1)
)
ORDER BY
CASE WHEN d0.ContractNumber = t.ContractNumber THEN 0 ELSE 1 END,
CASE WHEN d0.IsDeleted = 0 THEN 0 ELSE 1 END,
d0.Id DESC
) d
OUTER APPLY (
-- تراکنش دایا که از قبل شارژ شده (مرجع کیف پول یا RefId قرارداد)
SELECT TOP (1) t0.Id AS TxId
FROM CMS.Transactions t0
WHERE t0.IsDeleted = 0
AND t0.PaymentStatus = 0
AND (
t0.RefId = t.ContractNumber
OR (
t0.Description LIKE N'%اعتبار دایا%'
AND EXISTS (
SELECT 1
FROM CMS.UserWallets w
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id
WHERE w.UserId = u.Id
AND h.RefrenceId = t0.Id
)
)
)
ORDER BY
CASE WHEN t0.RefId = t.ContractNumber THEN 0 ELSE 1 END,
t0.Id DESC
) etx;
PRINT '=== Before ===';
SELECT Verdict, COUNT(*) AS Cnt FROM @Work GROUP BY Verdict;
SELECT ContractNumber, NationalCode, UserId, DayaContractId, HasCredit, IsProcessed, HasDayaUpp,
ExistingDayaTxId, Verdict
FROM @Work
ORDER BY Verdict, ContractNumber;
BEGIN TRAN;
DECLARE @ContractNumber NVARCHAR(50), @NationalCode NVARCHAR(20), @UserId BIGINT;
DECLARE @DayaContractId BIGINT, @HasCredit BIT, @IsProcessed BIT, @HasDayaUpp BIT, @Verdict NVARCHAR(20);
DECLARE @ExistingDayaTxId BIGINT;
DECLARE @TxId BIGINT, @WalletId BIGINT, @Action NVARCHAR(200);
DECLARE @Log TABLE (
ContractNumber NVARCHAR(50),
UserId BIGINT,
VerdictBefore NVARCHAR(20),
ActionTaken NVARCHAR(200),
TxId BIGINT NULL
);
DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
SELECT ContractNumber, NationalCode, UserId, DayaContractId, HasCredit, IsProcessed, HasDayaUpp, ExistingDayaTxId, Verdict
FROM @Work
WHERE Verdict IN (N'NOT_PROCESSED', N'PARTIAL');
OPEN cur;
FETCH NEXT FROM cur INTO @ContractNumber, @NationalCode, @UserId, @DayaContractId, @HasCredit, @IsProcessed, @HasDayaUpp, @ExistingDayaTxId, @Verdict;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @TxId = NULL;
SET @Action = N'';
IF @UserId IS NULL
BEGIN
INSERT INTO @Log VALUES (@ContractNumber, NULL, @Verdict, N'SKIP NO_USER', NULL);
GOTO NextRow;
END
------------------------------------------------------------
-- Ensure / revive DayaLoanContract
------------------------------------------------------------
IF @DayaContractId IS NULL
BEGIN
INSERT INTO CMS.DayaLoanContracts
(
UserId, NationalCode, ContractNumber, Status, IsProcessed,
LastCheckDate, ProcessedDate, TransactionId,
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
VALUES
(
@UserId, @NationalCode, @ContractNumber, 2, 0,
@Now, NULL, NULL,
@Now, NULL, @Now, NULL, 0
);
SET @DayaContractId = SCOPE_IDENTITY();
SET @Action = @Action + N'INSERT_CONTRACT;';
END
ELSE
BEGIN
UPDATE CMS.DayaLoanContracts
SET
IsDeleted = 0,
NationalCode = @NationalCode,
ContractNumber = @ContractNumber,
Status = 2, -- Received («فعال شده»)
LastCheckDate = @Now,
LastModified = @Now
WHERE Id = @DayaContractId;
SET @Action = @Action + N'UPDATE_CONTRACT;';
END
------------------------------------------------------------
-- اگر تراکنش/شارژ «اعتبار دایا» از قبل هست → همان Tx را بگیر؛ شارژ نکن
------------------------------------------------------------
IF @ExistingDayaTxId IS NOT NULL
BEGIN
SET @TxId = @ExistingDayaTxId;
SET @Action = @Action + CONCAT(N'REUSE_EXISTING_DAYA_TX=', @TxId, N';');
UPDATE CMS.Users
SET
PackagePurchaseMethod = 1,
HasReceivedDayaCredit = 1,
DayaCreditReceivedAt = COALESCE(DayaCreditReceivedAt, @Now),
LastModified = @Now
WHERE Id = @UserId;
SET @HasCredit = 1;
SET @Action = @Action + N'SET_USER_FLAGS(no charge);';
END
ELSE IF @HasCredit = 0
BEGIN
-- Full wallet charge only if never credited and no existing Daya tx
INSERT INTO CMS.Transactions
(
Amount, Description, PaymentStatus, PaymentDate, RefId, Type,
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
VALUES
(
@PackagePrice,
CONCAT(N'دریافت اعتبار دایا - قرارداد ', @ContractNumber),
0, -- Success
@Now,
@ContractNumber,
2, -- DepositExternal1
@Now, NULL, @Now, NULL, 0
);
SET @TxId = SCOPE_IDENTITY();
SELECT @WalletId = Id FROM CMS.UserWallets WHERE UserId = @UserId;
IF @WalletId IS NULL
BEGIN
INSERT INTO CMS.UserWallets (UserId, Balance, NetworkBalance, DiscountBalance, WalletMode, Created, LastModified, IsDeleted)
VALUES (@UserId, 0, 0, 0, 0, @Now, @Now, 0);
SET @WalletId = SCOPE_IDENTITY();
END
UPDATE CMS.UserWallets
SET
Balance = Balance + @PackagePrice,
DiscountBalance = DiscountBalance + @DiscountAmount,
LastModified = @Now
WHERE Id = @WalletId;
INSERT INTO CMS.UserWalletHistories
(
WalletId, CurrentBalance, ChangeValue, CurrentNetworkBalance, ChangeNerworkValue,
CurrentDiscountBalance, ChangeDiscountValue, IsIncrease, RefrenceId, PackageId,
Created, LastModified, IsDeleted
)
VALUES
(
@WalletId, 0, @PackagePrice, 0, 0,
0, 0, 1, @TxId, @PackageId,
@Now, @Now, 0
),
(
@WalletId, 0, 0, 0, 0,
0, @DiscountAmount, 1, @TxId, @PackageId,
@Now, @Now, 0
);
UPDATE CMS.Users
SET
HasReceivedDayaCredit = 1,
DayaCreditReceivedAt = @Now,
PackagePurchaseMethod = 1, -- DayaLoan
LastModified = @Now
WHERE Id = @UserId;
SET @HasCredit = 1;
SET @Action = @Action + N'CHARGE_WALLET+USER_FLAGS;';
END
ELSE
BEGIN
-- HasReceivedDayaCredit=1 ولی ExistingDayaTx پیدا نشد
UPDATE CMS.Users
SET
PackagePurchaseMethod = 1,
HasReceivedDayaCredit = 1,
DayaCreditReceivedAt = COALESCE(DayaCreditReceivedAt, @Now),
LastModified = @Now
WHERE Id = @UserId;
SET @Action = @Action + N'SKIP_CHARGE(HasReceivedDayaCredit);';
END
------------------------------------------------------------
-- Ensure successful Daya UPP (no duplicate if already exists)
------------------------------------------------------------
IF @HasDayaUpp = 0
BEGIN
IF @TxId IS NULL
BEGIN
-- فقط لجر؛ بدون شارژ کیف (نباید برای کسی که Tx دایا دارد به اینجا برسد)
INSERT INTO CMS.Transactions
(
Amount, Description, PaymentStatus, PaymentDate, RefId, Type,
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
VALUES
(
@PackagePrice,
CONCAT(N'لجر UPP دایا (بدون شارژ مجدد) - قرارداد ', @ContractNumber),
0,
@Now,
CONCAT(N'UPP-DAYA-FIX-', @ContractNumber),
2,
@Now, NULL, @Now, NULL, 0
);
SET @TxId = SCOPE_IDENTITY();
SET @Action = @Action + N'LEDGER_TX;';
END
INSERT INTO CMS.UserPackagePurchases
(
UserId, PackageId, PurchaseMethod, PurchasedAt, Amount,
OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
)
VALUES
(
@UserId, @PackageId, 1, @Now, @PackagePrice,
NULL, @TxId, @Now, NULL, @Now, NULL, 0
);
SET @Action = @Action + N'INSERT_UPP;';
END
------------------------------------------------------------
-- Mark contract processed
------------------------------------------------------------
UPDATE CMS.DayaLoanContracts
SET
IsProcessed = 1,
ProcessedDate = COALESCE(ProcessedDate, @Now),
TransactionId = COALESCE(TransactionId, @TxId),
Status = 2,
ContractNumber = @ContractNumber,
IsDeleted = 0,
LastModified = @Now
WHERE Id = @DayaContractId;
SET @Action = @Action + N'MARK_PROCESSED;';
INSERT INTO @Log VALUES (@ContractNumber, @UserId, @Verdict, @Action, @TxId);
NextRow:
FETCH NEXT FROM cur INTO @ContractNumber, @NationalCode, @UserId, @DayaContractId, @HasCredit, @IsProcessed, @HasDayaUpp, @ExistingDayaTxId, @Verdict;
END
CLOSE cur;
DEALLOCATE cur;
PRINT '=== Actions ===';
SELECT * FROM @Log ORDER BY VerdictBefore, ContractNumber;
-- Re-score after changes
PRINT '=== After (same targets) ===';
SELECT
t.ContractNumber,
t.NationalCode,
u.Id AS UserId,
u.HasReceivedDayaCredit,
d.IsProcessed,
d.ContractNumber AS DbContractNumber,
d.Status,
CASE WHEN EXISTS (
SELECT 1 FROM CMS.UserPackagePurchases upp
LEFT JOIN CMS.Transactions tx ON tx.Id = upp.TransactionId AND tx.IsDeleted = 0
WHERE upp.UserId = u.Id AND upp.IsDeleted = 0 AND upp.PurchaseMethod = 1
AND (upp.TransactionId IS NULL OR tx.PaymentStatus = 0)
) THEN 1 ELSE 0 END AS HasDayaUpp,
CASE
WHEN u.Id IS NULL THEN N'NO_USER'
WHEN u.HasReceivedDayaCredit = 1 AND d.IsProcessed = 1
AND EXISTS (
SELECT 1 FROM CMS.UserPackagePurchases upp
LEFT JOIN CMS.Transactions tx ON tx.Id = upp.TransactionId AND tx.IsDeleted = 0
WHERE upp.UserId = u.Id AND upp.IsDeleted = 0 AND upp.PurchaseMethod = 1
AND (upp.TransactionId IS NULL OR tx.PaymentStatus = 0)
) THEN N'DONE'
WHEN u.HasReceivedDayaCredit = 1 OR d.IsProcessed = 1 THEN N'PARTIAL'
ELSE N'NOT_PROCESSED'
END AS Verdict
FROM @Targets t
LEFT JOIN CMS.Users u ON u.IsDeleted = 0 AND (
(t.UserIdHint IS NOT NULL AND u.Id = t.UserIdHint) OR (t.UserIdHint IS NULL AND u.NationalCode = t.NationalCode)
)
LEFT JOIN CMS.DayaLoanContracts d ON d.IsDeleted = 0 AND d.UserId = u.Id
AND (d.ContractNumber = t.ContractNumber OR d.NationalCode = t.NationalCode)
ORDER BY Verdict, t.ContractNumber;
IF @Commit = 1
BEGIN
COMMIT;
PRINT 'COMMITTED.';
END
ELSE
BEGIN
ROLLBACK;
PRINT 'Dry-run ROLLBACK. Set @Commit = 1 to apply.';
END
@@ -1,157 +0,0 @@
/*
Correct users wrongly marked as DayaLoan — should be Manual.
Affects:
- CMS.Users.PackagePurchaseMethod (1 → 3)
- CMS.ClubMemberships.PurchaseMethod (1 or 0 → 3)
- CMS.ClubMembershipCycles.PurchaseMethod (1 or 0 → 3)
- CMS.UserPackagePurchases.PurchaseMethod (1 → 3)
- CMS.Users.HasReceivedDayaCredit / DayaCreditReceivedAt clear
- CMS.DayaLoanContracts soft-delete stub rows
(IsProcessed=0, no TransactionId, no ProcessedDate)
for these UserIds only
PackagePurchaseMethod: None=0, DayaLoan=1, DirectPurchase=2, Manual=3
NOTE: UserId=43 shares NationalCode 5289780335 with UserId=42 (real Excel Daya).
This script only touches UserId=43's own rows — not UserId=42's contract.
Defaults to ROLLBACK. Uncomment COMMIT after verifying PRINT counts.
*/
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRAN;
DECLARE @FixUsers TABLE (UserId BIGINT PRIMARY KEY);
INSERT INTO @FixUsers (UserId) VALUES
(2),(7),(8),(9),(10),(11),(12),(13),(40),(41),(43),(45),(47),
(52),(58),(69),(71),(72),(74),(75),(87),(88),(89),(90),(91),(93),
(96),(99),(103),(105),(106),(111),(113),(115),(116),(119),(121),
(125),(130),(142),(162),(169),(170),(171),(172),(175),(176);
------------------------------------------------------------
-- 1) Users
------------------------------------------------------------
UPDATE u
SET
u.PackagePurchaseMethod = 3, -- Manual
u.HasReceivedDayaCredit = 0,
u.DayaCreditReceivedAt = NULL,
u.LastModified = SYSUTCDATETIME()
FROM CMS.Users u
INNER JOIN @FixUsers f ON f.UserId = u.Id
WHERE u.IsDeleted = 0
AND (
u.PackagePurchaseMethod = 1 -- DayaLoan
OR u.HasReceivedDayaCredit = 1
OR u.DayaCreditReceivedAt IS NOT NULL
);
PRINT CONCAT('Users updated: ', @@ROWCOUNT);
------------------------------------------------------------
-- 2) ClubMemberships
------------------------------------------------------------
UPDATE m
SET
m.PurchaseMethod = 3, -- Manual
m.LastModified = SYSUTCDATETIME()
FROM CMS.ClubMemberships m
INNER JOIN @FixUsers f ON f.UserId = m.UserId
WHERE m.IsDeleted = 0
AND m.PurchaseMethod IN (0, 1); -- None or DayaLoan
PRINT CONCAT('ClubMemberships updated: ', @@ROWCOUNT);
------------------------------------------------------------
-- 3) ClubMembershipCycles
------------------------------------------------------------
UPDATE c
SET
c.PurchaseMethod = 3, -- Manual
c.LastModified = SYSUTCDATETIME()
FROM CMS.ClubMembershipCycles c
INNER JOIN @FixUsers f ON f.UserId = c.UserId
WHERE c.IsDeleted = 0
AND c.PurchaseMethod IN (0, 1);
PRINT CONCAT('ClubMembershipCycles updated: ', @@ROWCOUNT);
------------------------------------------------------------
-- 4) UserPackagePurchases (incl. backfill rows)
------------------------------------------------------------
UPDATE upp
SET
upp.PurchaseMethod = 3, -- Manual
upp.LastModified = SYSUTCDATETIME()
FROM CMS.UserPackagePurchases upp
INNER JOIN @FixUsers f ON f.UserId = upp.UserId
WHERE upp.IsDeleted = 0
AND upp.PurchaseMethod = 1; -- only Daya → Manual (keep Direct=2 if any)
PRINT CONCAT('UserPackagePurchases updated: ', @@ROWCOUNT);
------------------------------------------------------------
-- 5) Soft-delete stub DayaLoanContracts (never processed)
------------------------------------------------------------
UPDATE d
SET
d.IsDeleted = 1,
d.LastModified = SYSUTCDATETIME()
FROM CMS.DayaLoanContracts d
INNER JOIN @FixUsers f ON f.UserId = d.UserId
WHERE d.IsDeleted = 0
AND d.IsProcessed = 0
AND d.TransactionId IS NULL
AND d.ProcessedDate IS NULL;
PRINT CONCAT('DayaLoanContracts soft-deleted (stubs): ', @@ROWCOUNT);
------------------------------------------------------------
-- Verify
------------------------------------------------------------
SELECT 'Users still Daya' AS CheckName, COUNT(*) AS Cnt
FROM CMS.Users u
INNER JOIN @FixUsers f ON f.UserId = u.Id
WHERE u.IsDeleted = 0 AND u.PackagePurchaseMethod = 1
UNION ALL
SELECT 'Club still Daya/None', COUNT(*)
FROM CMS.ClubMemberships m
INNER JOIN @FixUsers f ON f.UserId = m.UserId
WHERE m.IsDeleted = 0 AND m.PurchaseMethod IN (0, 1)
UNION ALL
SELECT 'Cycle still Daya/None', COUNT(*)
FROM CMS.ClubMembershipCycles c
INNER JOIN @FixUsers f ON f.UserId = c.UserId
WHERE c.IsDeleted = 0 AND c.PurchaseMethod IN (0, 1)
UNION ALL
SELECT 'UPP still Daya', COUNT(*)
FROM CMS.UserPackagePurchases upp
INNER JOIN @FixUsers f ON f.UserId = upp.UserId
WHERE upp.IsDeleted = 0 AND upp.PurchaseMethod = 1
UNION ALL
SELECT 'Active stub Daya contracts', COUNT(*)
FROM CMS.DayaLoanContracts d
INNER JOIN @FixUsers f ON f.UserId = d.UserId
WHERE d.IsDeleted = 0
AND d.IsProcessed = 0
AND d.TransactionId IS NULL
AND d.ProcessedDate IS NULL;
-- Sample after
SELECT u.Id AS UserId,
u.FirstName, u.LastName, u.Mobile,
u.PackagePurchaseMethod,
m.PurchaseMethod AS ClubPurchaseMethod
FROM CMS.Users u
INNER JOIN @FixUsers f ON f.UserId = u.Id
LEFT JOIN CMS.ClubMemberships m ON m.UserId = u.Id AND m.IsDeleted = 0
WHERE u.IsDeleted = 0
ORDER BY u.Id;
-- Uncomment to apply:
-- COMMIT;
ROLLBACK;
PRINT 'Rolled back (safety). Uncomment COMMIT and remove ROLLBACK to apply.';
@@ -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.';