Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea7ba3bd3f | |||
| fe7179f6e9 | |||
| f79affb44d | |||
| 749cfeb8f4 | |||
| 6f573c0a29 | |||
| 9b82b45e46 | |||
| 39bbe59bbc | |||
| 5c852e499d | |||
| cbc1225562 | |||
| 6ad1341754 | |||
| e970e516ce | |||
| 9d17f174af | |||
| 29426022aa | |||
| 2b958bd214 | |||
| f31fb5ee39 | |||
| 0b7212ac1c | |||
| 59ed3857d4 | |||
| 78d1fee89a | |||
| 612d6fd141 | |||
| 87d61abc5b | |||
| ea1d2d4400 | |||
| c814cd9118 | |||
| 2d23dbc798 | |||
| 8ec0910cdf | |||
| e882f09955 | |||
| 436e5d7aca | |||
| 6a04a6a270 | |||
| 427d2cce1d | |||
| fb9e3d0109 | |||
| 6c90e4caf0 | |||
| 40da1bd8df |
@@ -6,15 +6,15 @@ on:
|
|||||||
- kub-stage
|
- kub-stage
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: 194.5.195.53:30080
|
REGISTRY: 185.252.31.44:30080
|
||||||
IMAGE_NAME: admin/cms
|
IMAGE_NAME: admin/cms
|
||||||
K8S_SERVER: 194.5.195.53
|
K8S_SERVER: 185.252.31.44
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: 194.5.195.53:32082/docker-sshpass:latest
|
image: 185.252.31.44:32082/docker-sshpass:latest
|
||||||
options: --privileged
|
options: --privileged
|
||||||
steps:
|
steps:
|
||||||
- name: Start Docker daemon
|
- name: Start Docker daemon
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
mkdir -p /etc/docker
|
mkdir -p /etc/docker
|
||||||
cat > /etc/docker/daemon.json << 'DAEMON'
|
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||||
{
|
{
|
||||||
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
|
"insecure-registries": ["185.252.31.44:30080", "185.252.31.44:32500", "185.252.31.44:32082"]
|
||||||
}
|
}
|
||||||
DAEMON
|
DAEMON
|
||||||
echo "🚀 Starting Docker daemon..."
|
echo "🚀 Starting Docker daemon..."
|
||||||
@@ -52,14 +52,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker registries
|
- name: Login to Docker registries
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 185.252.31.44:32082 -u admin --password-stdin
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||||
|
|
||||||
- name: Publish Protobuf packages
|
- name: Publish Protobuf packages
|
||||||
run: |
|
run: |
|
||||||
echo "📦 Publishing Protobuf packages..."
|
echo "📦 Publishing Protobuf packages..."
|
||||||
docker run --rm --network host -e CI=true -v $(pwd):/src -w /src \
|
docker run --rm --network host -e CI=true -v $(pwd):/src -w /src \
|
||||||
194.5.195.53:32082/dotnet/sdk:9.0 sh -c '
|
185.252.31.44:32082/dotnet/sdk:9.0 sh -c '
|
||||||
for proj in $(find . -name "*Protobuf*.csproj" -type f); do
|
for proj in $(find . -name "*Protobuf*.csproj" -type f); do
|
||||||
echo "📦 $proj"
|
echo "📦 $proj"
|
||||||
dotnet restore "$proj" --configfile src/NuGet.config
|
dotnet restore "$proj" --configfile src/NuGet.config
|
||||||
@@ -93,11 +93,11 @@ jobs:
|
|||||||
|
|
||||||
# Apply config (Secret) first, then deployment
|
# Apply config (Secret) first, then deployment
|
||||||
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
||||||
kubectl apply -f /tmp/cms-config.yaml &&
|
kubectl apply -n kbs-stage -f /tmp/cms-config.yaml &&
|
||||||
kubectl apply -f /tmp/cms-deployment.yaml &&
|
kubectl apply -n kbs-stage -f /tmp/cms-deployment.yaml &&
|
||||||
crictl rmi ${REGISTRY}/${IMAGE_NAME}:latest 2>/dev/null || true &&
|
crictl rmi ${REGISTRY}/${IMAGE_NAME}:latest 2>/dev/null || true &&
|
||||||
kubectl rollout restart deployment/cms &&
|
kubectl -n kbs-stage rollout restart deployment/cms &&
|
||||||
kubectl rollout status deployment/cms --timeout=180s &&
|
kubectl -n kbs-stage rollout status deployment/cms --timeout=180s &&
|
||||||
rm -f /tmp/cms-config.yaml /tmp/cms-deployment.yaml
|
rm -f /tmp/cms-config.yaml /tmp/cms-deployment.yaml
|
||||||
"
|
"
|
||||||
echo "✅ Deployed!"
|
echo "✅ Deployed!"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
- production
|
- production
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: 194.5.195.53:30080
|
REGISTRY: 185.252.31.44:30080
|
||||||
IMAGE_NAME: admin/cms
|
IMAGE_NAME: admin/cms
|
||||||
K8S_SERVER: 45.149.79.127
|
K8S_SERVER: 45.149.79.127
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: 194.5.195.53:32082/docker-sshpass:latest
|
image: 185.252.31.44:32082/docker-sshpass:latest
|
||||||
options: --privileged
|
options: --privileged
|
||||||
steps:
|
steps:
|
||||||
- name: Start Docker daemon
|
- name: Start Docker daemon
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
mkdir -p /etc/docker
|
mkdir -p /etc/docker
|
||||||
cat > /etc/docker/daemon.json << 'DAEMON'
|
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||||
{
|
{
|
||||||
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
|
"insecure-registries": ["185.252.31.44:30080", "185.252.31.44:32500", "185.252.31.44:32082"]
|
||||||
}
|
}
|
||||||
DAEMON
|
DAEMON
|
||||||
echo "🚀 Starting Docker daemon..."
|
echo "🚀 Starting Docker daemon..."
|
||||||
@@ -50,14 +50,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker registries
|
- name: Login to Docker registries
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 185.252.31.44:32082 -u admin --password-stdin
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||||
|
|
||||||
- name: Publish Protobuf packages
|
- name: Publish Protobuf packages
|
||||||
run: |
|
run: |
|
||||||
echo "📦 Publishing Protobuf packages..."
|
echo "📦 Publishing Protobuf packages..."
|
||||||
docker run --rm --network host -v $(pwd):/src -w /src \
|
docker run --rm --network host -v $(pwd):/src -w /src \
|
||||||
194.5.195.53:32082/dotnet/sdk:9.0 sh -c '
|
185.252.31.44:32082/dotnet/sdk:9.0 sh -c '
|
||||||
for proj in $(find . -name "*Protobuf*.csproj" -type f); do
|
for proj in $(find . -name "*Protobuf*.csproj" -type f); do
|
||||||
echo "📦 $proj"
|
echo "📦 $proj"
|
||||||
dotnet restore "$proj" --configfile src/NuGet.config
|
dotnet restore "$proj" --configfile src/NuGet.config
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
dotnet pack "$proj" -c Release --no-build -o "$(dirname $proj)/nupkg"
|
dotnet pack "$proj" -c Release --no-build -o "$(dirname $proj)/nupkg"
|
||||||
for nupkg in $(dirname $proj)/nupkg/*.nupkg; do
|
for nupkg in $(dirname $proj)/nupkg/*.nupkg; do
|
||||||
[ -f "$nupkg" ] && dotnet nuget push "$nupkg" \
|
[ -f "$nupkg" ] && dotnet nuget push "$nupkg" \
|
||||||
--source "http://194.5.195.53:32081/repository/foursat-nuget-hosted/index.json" \
|
--source "http://185.252.31.44:32081/repository/foursat-nuget-hosted/index.json" \
|
||||||
--api-key "admin:87zH26nbqT" \
|
--api-key "admin:87zH26nbqT" \
|
||||||
--skip-duplicate --allow-insecure-connections || true
|
--skip-duplicate --allow-insecure-connections || true
|
||||||
done
|
done
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build
|
FROM 185.252.31.44:32082/dotnet/sdk:9.0 AS build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# Copy NuGet config first
|
# Copy NuGet config first
|
||||||
@@ -11,7 +11,7 @@ COPY src/ ./
|
|||||||
RUN dotnet restore "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj" --configfile NuGet.config
|
RUN dotnet restore "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj" --configfile NuGet.config
|
||||||
RUN dotnet publish "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj" -c Release -o /app/publish --no-restore
|
RUN dotnet publish "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj" -c Release -o /app/publish --no-restore
|
||||||
|
|
||||||
FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS runtime
|
FROM 185.252.31.44:32082/dotnet/aspnet:9.0 AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /app/publish .
|
COPY --from=build /app/publish .
|
||||||
ENV ASPNETCORE_URLS=http://+:8080
|
ENV ASPNETCORE_URLS=http://+:8080
|
||||||
|
|||||||
@@ -24,6 +24,24 @@ stringData:
|
|||||||
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
||||||
"UseSandbox": false
|
"UseSandbox": false
|
||||||
},
|
},
|
||||||
|
"TwentyCard": {
|
||||||
|
"BaseUrl": "https://gateway.20card.ir/ipg",
|
||||||
|
"TerminalId": "165674",
|
||||||
|
"TerminalUsername": "ea5y1biehi",
|
||||||
|
"TerminalPassword": "SaLamAT@1404!",
|
||||||
|
"CreditPortalUrl": "https://userpanel.20card.ir/",
|
||||||
|
"DefaultEmail": "info@kbs2.ir",
|
||||||
|
"Packages": {
|
||||||
|
"4": {
|
||||||
|
"GatewayAmountToman": 11200000,
|
||||||
|
"WalletCreditToman": 12656000
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"GatewayAmountToman": 11200000,
|
||||||
|
"WalletCreditToman": 12656000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CmsBaseUrl": "https://cms.kbs2.ir",
|
"CmsBaseUrl": "https://cms.kbs2.ir",
|
||||||
"FrontOfficeBaseUrl": "https://kbs2.ir",
|
"FrontOfficeBaseUrl": "https://kbs2.ir",
|
||||||
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: cms
|
- name: cms
|
||||||
image: 194.5.195.53:30080/admin/cms:prod
|
image: 185.252.31.44:30080/admin/cms:prod
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ apiVersion: v1
|
|||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
name: cms-appsettings
|
name: cms-appsettings
|
||||||
namespace: default
|
namespace: kbs-stage
|
||||||
labels:
|
labels:
|
||||||
app: cms
|
app: cms
|
||||||
environment: staging
|
environment: staging
|
||||||
@@ -24,6 +24,25 @@ stringData:
|
|||||||
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
||||||
"UseSandbox": true
|
"UseSandbox": true
|
||||||
},
|
},
|
||||||
|
"TwentyCard": {
|
||||||
|
"BaseUrl": "https://gateway.20card.ir/ipg",
|
||||||
|
"TerminalId": "165674",
|
||||||
|
"TerminalUsername": "ea5y1biehi",
|
||||||
|
"TerminalPassword": "SaLamAT@1404!",
|
||||||
|
"CreditPortalUrl": "https://userpanel.20card.ir/",
|
||||||
|
"CallbackBaseUrl": "https://kbs2.ir",
|
||||||
|
"DefaultEmail": "info@kbs2.ir",
|
||||||
|
"Packages": {
|
||||||
|
"4": {
|
||||||
|
"GatewayAmountToman": 11200000,
|
||||||
|
"WalletCreditToman": 12656000
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"GatewayAmountToman": 11200000,
|
||||||
|
"WalletCreditToman": 12656000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CmsBaseUrl": "https://cms.se.kbs1.ir",
|
"CmsBaseUrl": "https://cms.se.kbs1.ir",
|
||||||
"FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir",
|
"FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir",
|
||||||
"FMS": {
|
"FMS": {
|
||||||
@@ -34,7 +53,7 @@ stringData:
|
|||||||
"JwtAudience": "https://localhost",
|
"JwtAudience": "https://localhost",
|
||||||
"JwtExpiryInDays": 5,
|
"JwtExpiryInDays": 5,
|
||||||
"ConnectionStrings": {
|
"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": "Data Source=mssql-svc.platform.svc.cluster.local,1433; Initial Catalog=Foursat;User ID=sa;Password=87zH26nbqT;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
|
||||||
"providerName": "System.Data.SqlClient"
|
"providerName": "System.Data.SqlClient"
|
||||||
},
|
},
|
||||||
"Otp": {
|
"Otp": {
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ apiVersion: v1
|
|||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
name: cms-uploads-pvc
|
name: cms-uploads-pvc
|
||||||
namespace: default
|
namespace: kbs-stage
|
||||||
labels:
|
labels:
|
||||||
app: cms
|
app: cms
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteOnce
|
- ReadWriteOnce
|
||||||
|
storageClassName: migrated-local
|
||||||
|
volumeName: pv-cms-uploads-migrated
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: 20Gi
|
storage: 20Gi
|
||||||
@@ -20,7 +22,7 @@ apiVersion: apps/v1
|
|||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
name: cms
|
name: cms
|
||||||
namespace: default
|
namespace: kbs-stage
|
||||||
labels:
|
labels:
|
||||||
app: cms
|
app: cms
|
||||||
spec:
|
spec:
|
||||||
@@ -33,10 +35,12 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: cms
|
app: cms
|
||||||
spec:
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: gitea-registry-secret
|
||||||
containers:
|
containers:
|
||||||
- name: cms
|
- name: cms
|
||||||
image: 194.5.195.53:30080/admin/cms:latest
|
image: 185.252.31.44:30080/admin/cms:latest
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: IfNotPresent
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
name: http
|
name: http
|
||||||
@@ -89,7 +93,7 @@ apiVersion: v1
|
|||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: cms-svc
|
name: cms-svc
|
||||||
namespace: default
|
namespace: kbs-stage
|
||||||
spec:
|
spec:
|
||||||
selector:
|
selector:
|
||||||
app: cms
|
app: cms
|
||||||
@@ -107,7 +111,7 @@ apiVersion: networking.k8s.io/v1
|
|||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
name: cms-ingress
|
name: cms-ingress
|
||||||
namespace: default
|
namespace: kbs-stage
|
||||||
annotations:
|
annotations:
|
||||||
kubernetes.io/ingress.class: "nginx"
|
kubernetes.io/ingress.class: "nginx"
|
||||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
@@ -115,6 +119,7 @@ metadata:
|
|||||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||||
spec:
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- cms.se.kbs1.ir
|
- cms.se.kbs1.ir
|
||||||
|
|||||||
@@ -0,0 +1,340 @@
|
|||||||
|
/*
|
||||||
|
Cleanup all side-effects of a test FrontOffice user (register + package + club + wallet + invitations).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
EXEC dbo.usp_CleanupTestUser @Mobile = N'09037437157', @DryRun = 1;
|
||||||
|
EXEC dbo.usp_CleanupTestUser @Mobile = N'09037437157';
|
||||||
|
EXEC dbo.usp_CleanupTestUser @Mobile = N'09...', @ReparentChildren = 1;
|
||||||
|
EXEC dbo.usp_CleanupTestUser @Mobile = N'09...', @ReversePool = 1, @AbortIfPoolCalculated = 1;
|
||||||
|
|
||||||
|
Pool reverse: for each ClubMembershipCycle, subtract Packages.ActivationFee
|
||||||
|
(fallback: ClubMembership.InitialContribution) from CMS.WeeklyCommissionPools
|
||||||
|
for matching WeekDefinition × PackageId.
|
||||||
|
|
||||||
|
Does NOT delete pool rows (shared). Does NOT claw back commission already paid to OTHER users.
|
||||||
|
Does NOT touch: Packages master, Contracts master, WeekDefinitions, Roles, catalogs.
|
||||||
|
*/
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.usp_CleanupTestUser
|
||||||
|
@UserId BIGINT = NULL,
|
||||||
|
@Mobile NVARCHAR(20) = NULL,
|
||||||
|
@HardDeleteUser BIT = 1,
|
||||||
|
@ReparentChildren BIT = 0,
|
||||||
|
@ReversePool BIT = 1,
|
||||||
|
@AbortIfPoolCalculated BIT = 0,
|
||||||
|
@DryRun BIT = 0
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
SET ANSI_NULLS ON;
|
||||||
|
|
||||||
|
IF @UserId IS NULL AND (@Mobile IS NULL OR LTRIM(RTRIM(@Mobile)) = N'')
|
||||||
|
BEGIN
|
||||||
|
RAISERROR(N'Provide @UserId and/or @Mobile.', 16, 1);
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
DECLARE @MobileNorm NVARCHAR(20) = NULL;
|
||||||
|
IF @Mobile IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
SET @MobileNorm = LTRIM(RTRIM(@Mobile));
|
||||||
|
IF LEFT(@MobileNorm, 3) = N'+98' SET @MobileNorm = N'0' + SUBSTRING(@MobileNorm, 4, 20);
|
||||||
|
IF LEFT(@MobileNorm, 2) = N'98' AND LEN(@MobileNorm) = 12 SET @MobileNorm = N'0' + SUBSTRING(@MobileNorm, 3, 20);
|
||||||
|
IF LEFT(@MobileNorm, 1) = N'9' AND LEN(@MobileNorm) = 10 SET @MobileNorm = N'0' + @MobileNorm;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF @UserId IS NULL
|
||||||
|
BEGIN
|
||||||
|
SELECT TOP (1) @UserId = Id
|
||||||
|
FROM CMS.Users
|
||||||
|
WHERE Mobile = @MobileNorm
|
||||||
|
OR Mobile = @Mobile
|
||||||
|
OR REPLACE(Mobile, N'+98', N'0') = @MobileNorm
|
||||||
|
ORDER BY IsDeleted ASC, Id DESC;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF @UserId IS NULL
|
||||||
|
BEGIN
|
||||||
|
RAISERROR(N'User not found for given @UserId/@Mobile.', 16, 1);
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
DECLARE @UserMobile NVARCHAR(20);
|
||||||
|
SELECT @UserMobile = Mobile FROM CMS.Users WHERE Id = @UserId;
|
||||||
|
IF @UserMobile IS NULL
|
||||||
|
BEGIN
|
||||||
|
RAISERROR(N'UserId %I64d not found.', 16, 1, @UserId);
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
DECLARE @ChildCount INT =
|
||||||
|
(SELECT COUNT(*) FROM CMS.Users WHERE NetworkParentId = @UserId AND IsDeleted = 0);
|
||||||
|
|
||||||
|
IF @ChildCount > 0 AND @ReparentChildren = 0 AND @HardDeleteUser = 1
|
||||||
|
BEGIN
|
||||||
|
RAISERROR(N'User has %d active network children. Pass @ReparentChildren=1 or soft-delete only.', 16, 1, @ChildCount);
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
PRINT N'=== usp_CleanupTestUser ===';
|
||||||
|
PRINT N'UserId=' + CAST(@UserId AS NVARCHAR(20)) + N' Mobile=' + ISNULL(@UserMobile, N'?')
|
||||||
|
+ N' DryRun=' + CAST(@DryRun AS NVARCHAR(1))
|
||||||
|
+ N' ReversePool=' + CAST(@ReversePool AS NVARCHAR(1));
|
||||||
|
|
||||||
|
DECLARE @Wallets TABLE (WalletId BIGINT PRIMARY KEY);
|
||||||
|
INSERT INTO @Wallets (WalletId)
|
||||||
|
SELECT Id FROM CMS.UserWallets WHERE UserId = @UserId;
|
||||||
|
|
||||||
|
DECLARE @Tx TABLE (TransactionId BIGINT PRIMARY KEY);
|
||||||
|
INSERT INTO @Tx (TransactionId)
|
||||||
|
SELECT DISTINCT x.TransactionId
|
||||||
|
FROM (
|
||||||
|
SELECT TransactionId FROM CMS.UserPackagePurchases WHERE UserId = @UserId AND TransactionId IS NOT NULL
|
||||||
|
UNION
|
||||||
|
SELECT TransactionId FROM CMS.PaymentTransactions WHERE UserId = @UserId AND TransactionId IS NOT NULL
|
||||||
|
UNION
|
||||||
|
SELECT TransactionId FROM CMS.UserOrders WHERE UserId = @UserId AND TransactionId IS NOT NULL
|
||||||
|
UNION
|
||||||
|
SELECT TransactionId FROM CMS.ManualPayments WHERE UserId = @UserId AND TransactionId IS NOT NULL
|
||||||
|
UNION
|
||||||
|
SELECT TransactionId FROM CMS.DayaLoanContracts WHERE UserId = @UserId AND TransactionId IS NOT NULL
|
||||||
|
UNION
|
||||||
|
SELECT TransactionId FROM CMS.DiscountOrders WHERE UserId = @UserId AND TransactionId IS NOT NULL
|
||||||
|
UNION
|
||||||
|
SELECT h.RefrenceId
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN @Wallets w ON w.WalletId = h.WalletId
|
||||||
|
WHERE h.RefrenceId IS NOT NULL
|
||||||
|
) x
|
||||||
|
WHERE x.TransactionId IS NOT NULL;
|
||||||
|
|
||||||
|
DECLARE @Orders TABLE (OrderId BIGINT PRIMARY KEY);
|
||||||
|
INSERT INTO @Orders (OrderId)
|
||||||
|
SELECT Id FROM CMS.UserOrders WHERE UserId = @UserId;
|
||||||
|
|
||||||
|
DECLARE @DiscountOrders TABLE (DiscountOrderId BIGINT PRIMARY KEY);
|
||||||
|
INSERT INTO @DiscountOrders (DiscountOrderId)
|
||||||
|
SELECT Id FROM CMS.DiscountOrders WHERE UserId = @UserId;
|
||||||
|
|
||||||
|
DECLARE @ClubMemberships TABLE (ClubMembershipId BIGINT PRIMARY KEY);
|
||||||
|
INSERT INTO @ClubMemberships (ClubMembershipId)
|
||||||
|
SELECT Id FROM CMS.ClubMemberships WHERE UserId = @UserId;
|
||||||
|
|
||||||
|
DECLARE @Cycles TABLE (CycleId BIGINT PRIMARY KEY);
|
||||||
|
INSERT INTO @Cycles (CycleId)
|
||||||
|
SELECT Id FROM CMS.ClubMembershipCycles WHERE UserId = @UserId
|
||||||
|
UNION
|
||||||
|
SELECT Id FROM CMS.ClubMembershipCycles WHERE ClubMembershipId IN (SELECT ClubMembershipId FROM @ClubMemberships);
|
||||||
|
|
||||||
|
DECLARE @Payouts TABLE (PayoutId BIGINT PRIMARY KEY);
|
||||||
|
INSERT INTO @Payouts (PayoutId)
|
||||||
|
SELECT Id FROM CMS.UserCommissionPayouts WHERE UserId = @UserId;
|
||||||
|
|
||||||
|
DECLARE @PoolReversals TABLE (
|
||||||
|
CycleId BIGINT,
|
||||||
|
PackageId BIGINT,
|
||||||
|
WeekDefinitionId BIGINT NULL,
|
||||||
|
PoolId BIGINT NULL,
|
||||||
|
Fee BIGINT,
|
||||||
|
PoolAmountBefore BIGINT NULL,
|
||||||
|
IsCalculated BIT NULL,
|
||||||
|
Note NVARCHAR(200)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO @PoolReversals (CycleId, PackageId, WeekDefinitionId, PoolId, Fee, PoolAmountBefore, IsCalculated, Note)
|
||||||
|
SELECT
|
||||||
|
c.Id,
|
||||||
|
c.PackageId,
|
||||||
|
wd.Id,
|
||||||
|
wp.Id,
|
||||||
|
ISNULL(NULLIF(cm.InitialContribution, 0), ISNULL(p.ActivationFee, 0)),
|
||||||
|
wp.TotalPoolAmount,
|
||||||
|
wp.IsCalculated,
|
||||||
|
CASE
|
||||||
|
WHEN wd.Id IS NULL THEN N'NO_WEEK_MATCH'
|
||||||
|
WHEN wp.Id IS NULL THEN N'NO_POOL_ROW'
|
||||||
|
WHEN wp.IsCalculated = 1 THEN N'POOL_ALREADY_CALCULATED'
|
||||||
|
ELSE N'OK'
|
||||||
|
END
|
||||||
|
FROM CMS.ClubMembershipCycles c
|
||||||
|
LEFT JOIN CMS.ClubMemberships cm ON cm.Id = c.ClubMembershipId
|
||||||
|
LEFT JOIN CMS.Packages p ON p.Id = c.PackageId
|
||||||
|
LEFT JOIN CMS.WeekDefinitions wd
|
||||||
|
ON CAST(c.PackagePurchasedAt AS DATE) >= CAST(wd.StartDate AS DATE)
|
||||||
|
AND CAST(c.PackagePurchasedAt AS DATE) <= CAST(wd.EndDate AS DATE)
|
||||||
|
AND wd.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.WeeklyCommissionPools wp
|
||||||
|
ON wp.WeekDefinitionId = wd.Id
|
||||||
|
AND wp.PackageId = c.PackageId
|
||||||
|
AND wp.IsDeleted = 0
|
||||||
|
WHERE c.Id IN (SELECT CycleId FROM @Cycles);
|
||||||
|
|
||||||
|
SELECT N'Users' AS Entity, COUNT(*) AS Cnt FROM CMS.Users WHERE Id = @UserId
|
||||||
|
UNION ALL SELECT N'OtpTokens', COUNT(*) FROM CMS.OtpTokens WHERE Mobile IN (@UserMobile, @MobileNorm, @Mobile)
|
||||||
|
UNION ALL SELECT N'UserInvitations', COUNT(*) FROM CMS.UserInvitations WHERE InviterUserId = @UserId OR InviteeUserId = @UserId
|
||||||
|
UNION ALL SELECT N'ClubMemberships', COUNT(*) FROM @ClubMemberships
|
||||||
|
UNION ALL SELECT N'ClubMembershipCycles', COUNT(*) FROM @Cycles
|
||||||
|
UNION ALL SELECT N'UserPackagePurchases', COUNT(*) FROM CMS.UserPackagePurchases WHERE UserId = @UserId
|
||||||
|
UNION ALL SELECT N'PaymentTransactions', COUNT(*) FROM CMS.PaymentTransactions WHERE UserId = @UserId OR Mobile = @UserMobile
|
||||||
|
UNION ALL SELECT N'UserWallets', COUNT(*) FROM @Wallets
|
||||||
|
UNION ALL SELECT N'UserWalletHistories', COUNT(*) FROM CMS.UserWalletHistories WHERE WalletId IN (SELECT WalletId FROM @Wallets)
|
||||||
|
UNION ALL SELECT N'Transactions(collected)', COUNT(*) FROM @Tx
|
||||||
|
UNION ALL SELECT N'NetworkChildren', @ChildCount
|
||||||
|
UNION ALL SELECT N'UserOrders', COUNT(*) FROM @Orders
|
||||||
|
UNION ALL SELECT N'DiscountOrders', COUNT(*) FROM @DiscountOrders
|
||||||
|
UNION ALL SELECT N'UserCommissionPayouts', COUNT(*) FROM @Payouts
|
||||||
|
UNION ALL SELECT N'PoolReversals(planned)', COUNT(*) FROM @PoolReversals WHERE Fee > 0;
|
||||||
|
|
||||||
|
SELECT N'--- Pool reverse plan ---' AS Info;
|
||||||
|
SELECT * FROM @PoolReversals;
|
||||||
|
|
||||||
|
IF @ReversePool = 1 AND @AbortIfPoolCalculated = 1
|
||||||
|
AND EXISTS (SELECT 1 FROM @PoolReversals WHERE IsCalculated = 1 AND Fee > 0)
|
||||||
|
BEGIN
|
||||||
|
RAISERROR(N'One or more target weekly pools are already calculated. Aborting (@AbortIfPoolCalculated=1).', 16, 1);
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF @DryRun = 1
|
||||||
|
BEGIN
|
||||||
|
PRINT N'DryRun=1 — no changes committed.';
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
BEGIN TRY
|
||||||
|
/* ---- Reverse weekly pool charges FIRST (needs cycle + package data) ---- */
|
||||||
|
IF @ReversePool = 1
|
||||||
|
BEGIN
|
||||||
|
UPDATE wp
|
||||||
|
SET wp.TotalPoolAmount = CASE
|
||||||
|
WHEN wp.TotalPoolAmount >= r.Fee THEN wp.TotalPoolAmount - r.Fee
|
||||||
|
ELSE 0
|
||||||
|
END,
|
||||||
|
wp.LastModified = SYSUTCDATETIME()
|
||||||
|
FROM CMS.WeeklyCommissionPools wp
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT PoolId, SUM(Fee) AS Fee
|
||||||
|
FROM @PoolReversals
|
||||||
|
WHERE PoolId IS NOT NULL AND Fee > 0
|
||||||
|
GROUP BY PoolId
|
||||||
|
) r ON r.PoolId = wp.Id;
|
||||||
|
|
||||||
|
PRINT N'Pool reverse applied for ' + CAST(@@ROWCOUNT AS NVARCHAR(20)) + N' pool row(s).';
|
||||||
|
END;
|
||||||
|
|
||||||
|
/* ---- Club ---- */
|
||||||
|
DELETE h FROM CMS.ClubMembershipCycleHistories h
|
||||||
|
WHERE h.UserId = @UserId OR h.ClubMembershipCycleId IN (SELECT CycleId FROM @Cycles);
|
||||||
|
|
||||||
|
DELETE FROM CMS.ClubMembershipCycles
|
||||||
|
WHERE Id IN (SELECT CycleId FROM @Cycles) OR UserId = @UserId;
|
||||||
|
|
||||||
|
DELETE FROM CMS.ClubMembershipHistories WHERE UserId = @UserId
|
||||||
|
OR ClubMembershipId IN (SELECT ClubMembershipId FROM @ClubMemberships);
|
||||||
|
|
||||||
|
DELETE FROM CMS.UserClubFeatures WHERE UserId = @UserId
|
||||||
|
OR ClubMembershipId IN (SELECT ClubMembershipId FROM @ClubMemberships);
|
||||||
|
|
||||||
|
DELETE FROM CMS.ClubMemberships WHERE UserId = @UserId;
|
||||||
|
|
||||||
|
/* ---- Commission payouts of THIS user only ---- */
|
||||||
|
DELETE h FROM CMS.CommissionPayoutHistories h
|
||||||
|
WHERE h.UserId = @UserId OR h.UserCommissionPayoutId IN (SELECT PayoutId FROM @Payouts);
|
||||||
|
|
||||||
|
DELETE FROM CMS.UserCommissionPayouts WHERE UserId = @UserId;
|
||||||
|
|
||||||
|
/* ---- Network ---- */
|
||||||
|
DELETE FROM CMS.NetworkWeeklyBalances WHERE UserId = @UserId;
|
||||||
|
DELETE FROM CMS.NetworkMembershipHistories
|
||||||
|
WHERE UserId = @UserId OR OldParentId = @UserId OR NewParentId = @UserId;
|
||||||
|
|
||||||
|
IF @ChildCount > 0 AND @ReparentChildren = 1
|
||||||
|
UPDATE CMS.Users SET NetworkParentId = NULL, LastModified = SYSUTCDATETIME()
|
||||||
|
WHERE NetworkParentId = @UserId;
|
||||||
|
|
||||||
|
/* ---- Invitations ---- */
|
||||||
|
DELETE FROM CMS.UserInvitations
|
||||||
|
WHERE InviterUserId = @UserId OR InviteeUserId = @UserId;
|
||||||
|
|
||||||
|
/* ---- Package / payment ---- */
|
||||||
|
DELETE FROM CMS.UserPackagePurchases WHERE UserId = @UserId;
|
||||||
|
DELETE FROM CMS.PaymentTransactions
|
||||||
|
WHERE UserId = @UserId OR Mobile = @UserMobile OR Mobile = @MobileNorm;
|
||||||
|
|
||||||
|
/* ---- Manual / Daya ---- */
|
||||||
|
DELETE FROM CMS.ManualPayments WHERE UserId = @UserId;
|
||||||
|
DELETE FROM CMS.DayaLoanContracts WHERE UserId = @UserId;
|
||||||
|
|
||||||
|
/* ---- Discount shop ---- */
|
||||||
|
DELETE d FROM CMS.DiscountOrderDetails d
|
||||||
|
WHERE d.DiscountOrderId IN (SELECT DiscountOrderId FROM @DiscountOrders);
|
||||||
|
DELETE FROM CMS.DiscountOrders WHERE UserId = @UserId;
|
||||||
|
DELETE FROM CMS.DiscountShoppingCarts WHERE UserId = @UserId;
|
||||||
|
|
||||||
|
/* ---- Stock ---- */
|
||||||
|
DELETE FROM CMS.StockMovements
|
||||||
|
WHERE PerformedByUserId = @UserId
|
||||||
|
OR OrderId IN (SELECT OrderId FROM @Orders)
|
||||||
|
OR DiscountOrderId IN (SELECT DiscountOrderId FROM @DiscountOrders);
|
||||||
|
|
||||||
|
/* ---- Orders ---- */
|
||||||
|
DELETE FROM CMS.FactorDetails WHERE OrderId IN (SELECT OrderId FROM @Orders);
|
||||||
|
DELETE FROM CMS.OrderVATs WHERE OrderId IN (SELECT OrderId FROM @Orders);
|
||||||
|
DELETE FROM CMS.UserOrders WHERE UserId = @UserId;
|
||||||
|
|
||||||
|
/* ---- Wallet ---- */
|
||||||
|
DELETE FROM CMS.UserWalletHistories WHERE WalletId IN (SELECT WalletId FROM @Wallets);
|
||||||
|
DELETE FROM CMS.UserWallets WHERE UserId = @UserId;
|
||||||
|
|
||||||
|
/* ---- Cart / address / roles / contracts ---- */
|
||||||
|
DELETE FROM CMS.UserCarts WHERE UserId = @UserId;
|
||||||
|
DELETE FROM CMS.UserAddresses WHERE UserId = @UserId;
|
||||||
|
DELETE FROM CMS.UserRoles WHERE UserId = @UserId;
|
||||||
|
DELETE FROM CMS.UserContracts WHERE UserId = @UserId;
|
||||||
|
|
||||||
|
/* ---- Content authored by test user ---- */
|
||||||
|
UPDATE CMS.BlogPosts SET IsDeleted = 1, LastModified = SYSUTCDATETIME()
|
||||||
|
WHERE AuthorUserId = @UserId AND IsDeleted = 0;
|
||||||
|
|
||||||
|
UPDATE CMS.PublicMessages SET IsDeleted = 1, LastModified = SYSUTCDATETIME()
|
||||||
|
WHERE CreatedByUserId = @UserId AND IsDeleted = 0;
|
||||||
|
|
||||||
|
/* ---- Transactions ---- */
|
||||||
|
DELETE t
|
||||||
|
FROM CMS.Transactions t
|
||||||
|
WHERE t.Id IN (SELECT TransactionId FROM @Tx)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM CMS.UserPackagePurchases p WHERE p.TransactionId = t.Id)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM CMS.PaymentTransactions p WHERE p.TransactionId = t.Id)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM CMS.UserOrders o WHERE o.TransactionId = t.Id)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM CMS.ManualPayments m WHERE m.TransactionId = t.Id)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM CMS.DayaLoanContracts d WHERE d.TransactionId = t.Id)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM CMS.DiscountOrders d WHERE d.TransactionId = t.Id);
|
||||||
|
|
||||||
|
/* ---- OTP ---- */
|
||||||
|
DELETE FROM CMS.OtpTokens
|
||||||
|
WHERE Mobile IN (@UserMobile, @MobileNorm, ISNULL(@Mobile, N''));
|
||||||
|
|
||||||
|
/* ---- User ---- */
|
||||||
|
IF @HardDeleteUser = 1
|
||||||
|
DELETE FROM CMS.Users WHERE Id = @UserId;
|
||||||
|
ELSE
|
||||||
|
UPDATE CMS.Users
|
||||||
|
SET IsDeleted = 1, LastModified = SYSUTCDATETIME()
|
||||||
|
WHERE Id = @UserId;
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
|
||||||
|
PRINT N'Cleanup completed successfully.';
|
||||||
|
SELECT N'DONE' AS Status, @UserId AS UserId, @UserMobile AS Mobile,
|
||||||
|
@HardDeleteUser AS HardDeleted, @ReversePool AS PoolReversed;
|
||||||
|
END TRY
|
||||||
|
BEGIN CATCH
|
||||||
|
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
|
||||||
|
DECLARE @Err NVARCHAR(4000) = ERROR_MESSAGE();
|
||||||
|
RAISERROR(N'Cleanup failed: %s', 16, 1, @Err);
|
||||||
|
END CATCH
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.4.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.4.0" />
|
||||||
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.10" />
|
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.10" />
|
||||||
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
||||||
|
|||||||
+2
-2
@@ -163,10 +163,10 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
request.UserId
|
request.UserId
|
||||||
);
|
);
|
||||||
|
|
||||||
// در فعالسازی اجباری، اگر PackagePurchaseMethod تنظیم نشده، مقدار DirectPurchase بگذار
|
// در فعالسازی اجباری ادمین، اگر روش خرید تنظیم نشده، دستی در نظر بگیر
|
||||||
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
|
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
|
||||||
{
|
{
|
||||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
user.PackagePurchaseMethod = PackagePurchaseMethod.Manual;
|
||||||
_context.Users.Update(user);
|
_context.Users.Update(user);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1,3 +1,5 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.CommissionCQ.Commands.RequestWithdrawal;
|
namespace CMSMicroservice.Application.CommissionCQ.Commands.RequestWithdrawal;
|
||||||
|
|
||||||
public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawalCommand, Unit>
|
public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawalCommand, Unit>
|
||||||
@@ -37,7 +39,10 @@ public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawal
|
|||||||
|
|
||||||
if (request.Method == WithdrawalMethod.Cash)
|
if (request.Method == WithdrawalMethod.Cash)
|
||||||
{
|
{
|
||||||
payout.IbanNumber = request.IbanNumber;
|
var normalizedIban = IbanNormalizer.TryNormalize(request.IbanNumber);
|
||||||
|
if (normalizedIban is null)
|
||||||
|
throw new InvalidOperationException("فرمت شماره شبا معتبر نیست. باید IR و ۲۴ رقم باشد.");
|
||||||
|
payout.IbanNumber = normalizedIban;
|
||||||
}
|
}
|
||||||
|
|
||||||
_context.UserCommissionPayouts.Update(payout);
|
_context.UserCommissionPayouts.Update(payout);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نرمالسازی و اعتبارسنجی شماره شبا ایران (IR + 24 رقم).
|
||||||
|
/// </summary>
|
||||||
|
public static class IbanNormalizer
|
||||||
|
{
|
||||||
|
private static readonly Regex IranianIbanRegex = new(@"^IR\d{24}$", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فاصله و خط تیره را حذف میکند، به حروف بزرگ تبدیل میکند و یک پیشوند IR میگذارد.
|
||||||
|
/// در صورت نامعتبر بودن، null برمیگرداند.
|
||||||
|
/// </summary>
|
||||||
|
public static string? TryNormalize(string? iban)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(iban))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var normalized = iban.Trim().ToUpperInvariant()
|
||||||
|
.Replace(" ", "", StringComparison.Ordinal)
|
||||||
|
.Replace("-", "", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
if (normalized.StartsWith("IR", StringComparison.Ordinal))
|
||||||
|
normalized = normalized[2..];
|
||||||
|
|
||||||
|
normalized = "IR" + normalized;
|
||||||
|
|
||||||
|
return IranianIbanRegex.IsMatch(normalized) ? normalized : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValid(string? iban) => TryNormalize(iban) is not null;
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ public interface IApplicationDbContext
|
|||||||
DbSet<Product> Products { get; }
|
DbSet<Product> Products { get; }
|
||||||
DbSet<ProductImage> ProductImages { get; }
|
DbSet<ProductImage> ProductImages { get; }
|
||||||
DbSet<User> Users { get; }
|
DbSet<User> Users { get; }
|
||||||
|
DbSet<UserInvitation> UserInvitations { get; }
|
||||||
DbSet<OtpToken> OtpTokens { get; }
|
DbSet<OtpToken> OtpTokens { get; }
|
||||||
DbSet<Contract> Contracts { get; }
|
DbSet<Contract> Contracts { get; }
|
||||||
DbSet<UserContract> UserContracts { get; }
|
DbSet<UserContract> UserContracts { get; }
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolve payment gateway by provider key (zarinpal, 20card, …).
|
||||||
|
/// </summary>
|
||||||
|
public interface IPaymentGatewayResolver
|
||||||
|
{
|
||||||
|
IPaymentGatewayService Get(string? provider);
|
||||||
|
}
|
||||||
@@ -98,6 +98,26 @@ public class PaymentRequest
|
|||||||
/// URL بازگشت بعد از پرداخت
|
/// URL بازگشت بعد از پرداخت
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string CallbackUrl { get; set; } = string.Empty;
|
public string CallbackUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نام پرداختکننده (برای درگاههایی مثل ۲۰کارت)
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شماره فاکتور / سفارش
|
||||||
|
/// </summary>
|
||||||
|
public string Invoice { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ایمیل (اختیاری — برای ۲۰کارت)
|
||||||
|
/// </summary>
|
||||||
|
public string? Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// کد ملی (اختیاری — برای ۲۰کارت)
|
||||||
|
/// </summary>
|
||||||
|
public string? NationalId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -35,6 +35,11 @@ public class UpdateOrderStatusCommandHandler : IRequestHandler<UpdateOrderStatus
|
|||||||
order.TrackingCode = request.TrackingCode;
|
order.TrackingCode = request.TrackingCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.AdminNotes))
|
||||||
|
{
|
||||||
|
order.DeliveryDescription = request.AdminNotes;
|
||||||
|
}
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
return new UpdateOrderStatusResponseDto
|
return new UpdateOrderStatusResponseDto
|
||||||
|
|||||||
+36
-12
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Application.Common.Models;
|
using CMSMicroservice.Application.Common.Models;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -80,16 +81,40 @@ public class GetAllDiscountOrdersQueryHandler : IRequestHandler<GetAllDiscountOr
|
|||||||
// Apply pagination
|
// Apply pagination
|
||||||
var pagination = request.PaginationQuery ?? new PaginationState { PageNumber = 1, PageSize = 20 };
|
var pagination = request.PaginationQuery ?? new PaginationState { PageNumber = 1, PageSize = 20 };
|
||||||
|
|
||||||
var orders = await query
|
var rows = await query
|
||||||
.OrderByDescending(o => o.Created)
|
.OrderByDescending(o => o.Created)
|
||||||
.Skip((pagination.PageNumber - 1) * pagination.PageSize)
|
.Skip((pagination.PageNumber - 1) * pagination.PageSize)
|
||||||
.Take(pagination.PageSize)
|
.Take(pagination.PageSize)
|
||||||
.Select(o => new AdminOrderDto
|
.Select(o => new
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
ShippingAddress = o.UserAddress.Address,
|
||||||
|
ReceiverName = o.UserAddress.Title,
|
||||||
|
o.TrackingCode,
|
||||||
|
AdminNote = o.DeliveryDescription,
|
||||||
|
o.Created,
|
||||||
|
o.LastModified,
|
||||||
|
ItemsCount = o.OrderDetails.Count
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var orders = rows.Select(o => new AdminOrderDto
|
||||||
{
|
{
|
||||||
Id = o.Id,
|
Id = o.Id,
|
||||||
UserId = o.UserId,
|
UserId = o.UserId,
|
||||||
UserFullName = (o.User.FirstName ?? "") + " " + (o.User.LastName ?? ""),
|
UserFullName = UserDisplayName.From(o.FirstName, o.LastName, o.Mobile, o.UserId),
|
||||||
UserMobile = o.User.Mobile,
|
UserMobile = o.Mobile,
|
||||||
TotalAmount = o.TotalAmount,
|
TotalAmount = o.TotalAmount,
|
||||||
DiscountBalanceUsed = o.DiscountBalanceUsed,
|
DiscountBalanceUsed = o.DiscountBalanceUsed,
|
||||||
GatewayAmountPaid = o.GatewayAmountPaid,
|
GatewayAmountPaid = o.GatewayAmountPaid,
|
||||||
@@ -97,17 +122,16 @@ public class GetAllDiscountOrdersQueryHandler : IRequestHandler<GetAllDiscountOr
|
|||||||
PaymentStatus = o.PaymentStatus,
|
PaymentStatus = o.PaymentStatus,
|
||||||
PaymentDate = o.PaymentDate,
|
PaymentDate = o.PaymentDate,
|
||||||
DeliveryStatus = o.DeliveryStatus,
|
DeliveryStatus = o.DeliveryStatus,
|
||||||
DeliveryDate = null, // TODO: Add DeliveryDate to DiscountOrder if needed
|
DeliveryDate = null,
|
||||||
ShippingAddress = o.UserAddress.Address,
|
ShippingAddress = o.ShippingAddress,
|
||||||
ReceiverName = o.UserAddress.Title,
|
ReceiverName = o.ReceiverName,
|
||||||
ReceiverMobile = o.User.Mobile,
|
ReceiverMobile = o.Mobile,
|
||||||
TrackingCode = o.TrackingCode,
|
TrackingCode = o.TrackingCode,
|
||||||
AdminNote = o.DeliveryDescription,
|
AdminNote = o.AdminNote,
|
||||||
Created = o.Created,
|
Created = o.Created,
|
||||||
LastModified = o.LastModified,
|
LastModified = o.LastModified,
|
||||||
ItemsCount = o.OrderDetails.Count
|
ItemsCount = o.ItemsCount
|
||||||
})
|
}).ToList();
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new GetAllDiscountOrdersResponseDto
|
return new GetAllDiscountOrdersResponseDto
|
||||||
{
|
{
|
||||||
|
|||||||
+2
@@ -32,6 +32,8 @@ public class UserAddressDto
|
|||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string Address { get; set; }
|
public string Address { get; set; }
|
||||||
public string PostalCode { get; set; }
|
public string PostalCode { get; set; }
|
||||||
|
/// <summary>شماره تماس سفارشدهنده (موبایل کاربر) برای برچسب پست</summary>
|
||||||
|
public string? Phone { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OrderItemDto
|
public class OrderItemDto
|
||||||
|
|||||||
+3
-1
@@ -25,6 +25,7 @@ public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, Order
|
|||||||
|
|
||||||
var order = await query
|
var order = await query
|
||||||
.Include(o => o.UserAddress)
|
.Include(o => o.UserAddress)
|
||||||
|
.Include(o => o.User)
|
||||||
.Include(o => o.OrderDetails)
|
.Include(o => o.OrderDetails)
|
||||||
.ThenInclude(od => od.Product)
|
.ThenInclude(od => od.Product)
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
@@ -50,7 +51,8 @@ public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, Order
|
|||||||
{
|
{
|
||||||
Title = order.UserAddress.Title,
|
Title = order.UserAddress.Title,
|
||||||
Address = order.UserAddress.Address,
|
Address = order.UserAddress.Address,
|
||||||
PostalCode = order.UserAddress.PostalCode
|
PostalCode = order.UserAddress.PostalCode,
|
||||||
|
Phone = order.User?.Mobile
|
||||||
},
|
},
|
||||||
Items = order.OrderDetails.Select(od => new OrderItemDto
|
Items = order.OrderDetails.Select(od => new OrderItemDto
|
||||||
{
|
{
|
||||||
|
|||||||
+12
-2
@@ -150,8 +150,18 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
|
|||||||
|
|
||||||
await _context.UserWalletHistories.AddAsync(walletLog, cancellationToken);
|
await _context.UserWalletHistories.AddAsync(walletLog, cancellationToken);
|
||||||
|
|
||||||
// 9. تنظیم روش خرید پکیج
|
// 9. تنظیم روش خرید پکیج + ثبت ledger خرید دستی
|
||||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
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
|
||||||
|
});
|
||||||
|
|
||||||
// 10. ذخیره همه تغییرات
|
// 10. ذخیره همه تغییرات
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|||||||
+1
-1
@@ -118,7 +118,7 @@ public class ProcessManualMembershipPaymentCommandHandler : IRequestHandler<Proc
|
|||||||
};
|
};
|
||||||
await _context.UserWalletHistories.AddAsync(balanceLog, cancellationToken);
|
await _context.UserWalletHistories.AddAsync(balanceLog, cancellationToken);
|
||||||
|
|
||||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
user.PackagePurchaseMethod = PackagePurchaseMethod.Manual;
|
||||||
// 10. بهروزرسانی ManualPayment با TransactionId
|
// 10. بهروزرسانی ManualPayment با TransactionId
|
||||||
manualPayment.TransactionId = transaction.Id;
|
manualPayment.TransactionId = transaction.Id;
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|||||||
+106
-8
@@ -1,4 +1,5 @@
|
|||||||
using CMSMicroservice.Domain.Enums;
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStatistics;
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStatistics;
|
||||||
|
|
||||||
@@ -34,17 +35,28 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
|
|||||||
}
|
}
|
||||||
var allDescendants = GetAllDescendants(userId, allUsers);
|
var allDescendants = GetAllDescendants(userId, allUsers);
|
||||||
|
|
||||||
// Statistics for the user's network (all descendants)
|
|
||||||
var totalMembers = allDescendants.Count;
|
|
||||||
var activeMembers = allDescendants.Count(x => !x.IsDeleted);
|
|
||||||
|
|
||||||
// Get direct left and right children
|
// Get direct left and right children
|
||||||
var leftChild = allUsers.FirstOrDefault(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Left);
|
var leftChild = allUsers.FirstOrDefault(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Left);
|
||||||
var rightChild = allUsers.FirstOrDefault(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Right);
|
var rightChild = allUsers.FirstOrDefault(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Right);
|
||||||
|
|
||||||
// Count all descendants in left and right subtrees
|
var leftLegUserIds = leftChild != null ? GetSubtreeUserIds(leftChild.Id, allUsers) : new HashSet<long>();
|
||||||
var leftLegCount = leftChild != null ? GetAllDescendants(leftChild.Id, allUsers).Count + 1 : 0; // +1 for leftChild itself
|
var rightLegUserIds = rightChild != null ? GetSubtreeUserIds(rightChild.Id, allUsers) : new HashSet<long>();
|
||||||
var rightLegCount = rightChild != null ? GetAllDescendants(rightChild.Id, allUsers).Count + 1 : 0; // +1 for rightChild itself
|
var allLegUserIds = leftLegUserIds.Union(rightLegUserIds).ToList();
|
||||||
|
|
||||||
|
// سازمان چپ/راست = فقط اعضای فعال باشگاه در هر پا
|
||||||
|
var activeClubUserIds = allLegUserIds.Count == 0
|
||||||
|
? new HashSet<long>()
|
||||||
|
: (await _context.ClubMemberships
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(cm => cm.IsActive && !cm.IsDeleted && allLegUserIds.Contains(cm.UserId))
|
||||||
|
.Select(cm => cm.UserId)
|
||||||
|
.ToListAsync(cancellationToken))
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
var leftLegCount = leftLegUserIds.Count(id => activeClubUserIds.Contains(id));
|
||||||
|
var rightLegCount = rightLegUserIds.Count(id => activeClubUserIds.Contains(id));
|
||||||
|
var totalMembers = leftLegCount + rightLegCount;
|
||||||
|
var activeMembers = totalMembers;
|
||||||
|
|
||||||
double leftPercentage = totalMembers > 0 ? (leftLegCount / (double)totalMembers) * 100 : 0;
|
double leftPercentage = totalMembers > 0 ? (leftLegCount / (double)totalMembers) * 100 : 0;
|
||||||
double rightPercentage = totalMembers > 0 ? (rightLegCount / (double)totalMembers) * 100 : 0;
|
double rightPercentage = totalMembers > 0 ? (rightLegCount / (double)totalMembers) * 100 : 0;
|
||||||
@@ -124,6 +136,9 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
|
|||||||
RightCount = x.RightCount
|
RightCount = x.RightCount
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
|
var (goldLeft, goldRight, silverLeft, silverRight) =
|
||||||
|
await LoadPackageHolderCountsByLegAsync(leftLegUserIds, rightLegUserIds, cancellationToken);
|
||||||
|
|
||||||
return new GetNetworkStatisticsResponseDto
|
return new GetNetworkStatisticsResponseDto
|
||||||
{
|
{
|
||||||
TotalMembers = totalMembers,
|
TotalMembers = totalMembers,
|
||||||
@@ -136,10 +151,93 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
|
|||||||
MaxDepth = maxDepth,
|
MaxDepth = maxDepth,
|
||||||
LevelDistribution = levelDistribution,
|
LevelDistribution = levelDistribution,
|
||||||
MonthlyGrowth = monthlyGrowth,
|
MonthlyGrowth = monthlyGrowth,
|
||||||
TopUsers = topUserModels
|
TopUsers = topUserModels,
|
||||||
|
GoldLeftLegTotal = goldLeft,
|
||||||
|
GoldRightLegTotal = goldRight,
|
||||||
|
SilverLeftLegTotal = silverLeft,
|
||||||
|
SilverRightLegTotal = silverRight
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تعداد اعضای فعال باشگاه با پکیج جاری (IsCurrentCycle) در پای چپ/راست.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<(int GoldLeft, int GoldRight, int SilverLeft, int SilverRight)> LoadPackageHolderCountsByLegAsync(
|
||||||
|
HashSet<long> leftLegUserIds,
|
||||||
|
HashSet<long> rightLegUserIds,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (leftLegUserIds.Count == 0 && rightLegUserIds.Count == 0)
|
||||||
|
return (0, 0, 0, 0);
|
||||||
|
|
||||||
|
var (goldPackageId, silverPackageId) = await ResolveGoldSilverPackageIdsAsync(cancellationToken);
|
||||||
|
if (!goldPackageId.HasValue && !silverPackageId.HasValue)
|
||||||
|
return (0, 0, 0, 0);
|
||||||
|
|
||||||
|
var allLegUserIds = leftLegUserIds.Union(rightLegUserIds).ToList();
|
||||||
|
|
||||||
|
var userPackages = await (
|
||||||
|
from cm in _context.ClubMemberships.AsNoTracking()
|
||||||
|
join cc in _context.ClubMembershipCycles.AsNoTracking() on cm.UserId equals cc.UserId
|
||||||
|
where cm.IsActive
|
||||||
|
&& !cm.IsDeleted
|
||||||
|
&& cc.IsCurrentCycle
|
||||||
|
&& allLegUserIds.Contains(cm.UserId)
|
||||||
|
select new { cm.UserId, cc.PackageId }
|
||||||
|
).ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var packageByUser = userPackages
|
||||||
|
.GroupBy(x => x.UserId)
|
||||||
|
.ToDictionary(g => g.Key, g => g.First().PackageId);
|
||||||
|
|
||||||
|
int goldLeft = 0, goldRight = 0, silverLeft = 0, silverRight = 0;
|
||||||
|
|
||||||
|
if (goldPackageId.HasValue)
|
||||||
|
{
|
||||||
|
goldLeft = leftLegUserIds.Count(id =>
|
||||||
|
packageByUser.TryGetValue(id, out var packageId) && packageId == goldPackageId.Value);
|
||||||
|
goldRight = rightLegUserIds.Count(id =>
|
||||||
|
packageByUser.TryGetValue(id, out var packageId) && packageId == goldPackageId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (silverPackageId.HasValue)
|
||||||
|
{
|
||||||
|
silverLeft = leftLegUserIds.Count(id =>
|
||||||
|
packageByUser.TryGetValue(id, out var packageId) && packageId == silverPackageId.Value);
|
||||||
|
silverRight = rightLegUserIds.Count(id =>
|
||||||
|
packageByUser.TryGetValue(id, out var packageId) && packageId == silverPackageId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (goldLeft, goldRight, silverLeft, silverRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(long? GoldPackageId, long? SilverPackageId)> ResolveGoldSilverPackageIdsAsync(
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var packages = await _context.Packages
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(p => !p.IsDeleted && p.IsActive)
|
||||||
|
.Select(p => new { p.Id, p.Title, p.IsBasePackage, p.MaxBalancesPerLeg })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var goldPackageId = packages.FirstOrDefault(p => p.IsBasePackage)?.Id
|
||||||
|
?? packages.FirstOrDefault(p => p.Title.Contains("طلایی") || p.Title.Contains("طلايی"))?.Id;
|
||||||
|
|
||||||
|
var silverPackageId = packages.FirstOrDefault(p =>
|
||||||
|
!p.IsBasePackage && (p.Title.Contains("نقره") || p.MaxBalancesPerLeg <= 30))?.Id
|
||||||
|
?? packages.FirstOrDefault(p => !p.IsBasePackage)?.Id;
|
||||||
|
|
||||||
|
return (goldPackageId, silverPackageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashSet<long> GetSubtreeUserIds(long rootUserId, List<User> allUsers)
|
||||||
|
{
|
||||||
|
var ids = new HashSet<long> { rootUserId };
|
||||||
|
foreach (var descendant in GetAllDescendants(rootUserId, allUsers))
|
||||||
|
ids.Add(descendant.Id);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recursively get all descendants of a user
|
/// Recursively get all descendants of a user
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+15
@@ -2,9 +2,13 @@ namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStat
|
|||||||
|
|
||||||
public class GetNetworkStatisticsResponseDto
|
public class GetNetworkStatisticsResponseDto
|
||||||
{
|
{
|
||||||
|
/// <summary>مجموع اعضای فعال باشگاه در پای چپ و راست (= LeftLegCount + RightLegCount)</summary>
|
||||||
public int TotalMembers { get; set; }
|
public int TotalMembers { get; set; }
|
||||||
|
/// <summary>همان TotalMembers — اعضای فعال باشگاه در شبکه کاربر</summary>
|
||||||
public int ActiveMembers { get; set; }
|
public int ActiveMembers { get; set; }
|
||||||
|
/// <summary>تعداد اعضای فعال باشگاه در زیردرخت پای چپ</summary>
|
||||||
public int LeftLegCount { get; set; }
|
public int LeftLegCount { get; set; }
|
||||||
|
/// <summary>تعداد اعضای فعال باشگاه در زیردرخت پای راست</summary>
|
||||||
public int RightLegCount { get; set; }
|
public int RightLegCount { get; set; }
|
||||||
public double LeftPercentage { get; set; }
|
public double LeftPercentage { get; set; }
|
||||||
public double RightPercentage { get; set; }
|
public double RightPercentage { get; set; }
|
||||||
@@ -13,6 +17,17 @@ public class GetNetworkStatisticsResponseDto
|
|||||||
public List<LevelDistributionModel> LevelDistribution { get; set; } = new();
|
public List<LevelDistributionModel> LevelDistribution { get; set; } = new();
|
||||||
public List<MonthlyGrowthModel> MonthlyGrowth { get; set; } = new();
|
public List<MonthlyGrowthModel> MonthlyGrowth { get; set; } = new();
|
||||||
public List<TopNetworkUserModel> TopUsers { get; set; } = new();
|
public List<TopNetworkUserModel> TopUsers { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>تعداد اعضای فعال باشگاه با پکیج طلایی جاری در پای چپ</summary>
|
||||||
|
public int GoldLeftLegTotal { get; set; }
|
||||||
|
/// <summary>تعداد اعضای فعال باشگاه با پکیج طلایی جاری در پای راست</summary>
|
||||||
|
public int GoldRightLegTotal { get; set; }
|
||||||
|
/// <summary>تعداد اعضای فعال باشگاه با پکیج نقرهای جاری در پای چپ</summary>
|
||||||
|
public int SilverLeftLegTotal { get; set; }
|
||||||
|
/// <summary>تعداد اعضای فعال باشگاه با پکیج نقرهای جاری در پای راست</summary>
|
||||||
|
public int SilverRightLegTotal { get; set; }
|
||||||
|
|
||||||
|
public string WeakerLeg => LeftLegCount <= RightLegCount ? "Left" : "Right";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LevelDistributionModel
|
public class LevelDistributionModel
|
||||||
|
|||||||
-1
@@ -1,5 +1,4 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Data.Common;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
|
||||||
|
|||||||
+12
@@ -60,6 +60,18 @@ public class NetworkTreeDto
|
|||||||
|
|
||||||
public long? PackageId { get; set; }
|
public long? PackageId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>تعداد اعضای فعال با پکیج طلایی جاری در پای چپ (stats bar)</summary>
|
||||||
|
public int GoldLeftLegTotal { get; set; }
|
||||||
|
|
||||||
|
/// <summary>تعداد اعضای فعال با پکیج طلایی جاری در پای راست (stats bar)</summary>
|
||||||
|
public int GoldRightLegTotal { get; set; }
|
||||||
|
|
||||||
|
/// <summary>تعداد اعضای فعال با پکیج نقرهای جاری در پای چپ (stats bar)</summary>
|
||||||
|
public int SilverLeftLegTotal { get; set; }
|
||||||
|
|
||||||
|
/// <summary>تعداد اعضای فعال با پکیج نقرهای جاری در پای راست (stats bar)</summary>
|
||||||
|
public int SilverRightLegTotal { get; set; }
|
||||||
|
|
||||||
public NetworkTreeDto? LeftChild { get; set; }
|
public NetworkTreeDto? LeftChild { get; set; }
|
||||||
public NetworkTreeDto? RightChild { get; set; }
|
public NetworkTreeDto? RightChild { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-8
@@ -8,6 +8,7 @@ using CMSMicroservice.Domain.Entities.Payment;
|
|||||||
using CMSMicroservice.Domain.Enums;
|
using CMSMicroservice.Domain.Enums;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
|
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
|
||||||
@@ -16,20 +17,23 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
|
|||||||
: IRequestHandler<VerifyUserPackagePurchasePaymentCommand, VerifyUserPackagePurchasePaymentResult>
|
: IRequestHandler<VerifyUserPackagePurchasePaymentCommand, VerifyUserPackagePurchasePaymentResult>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IPaymentGatewayService _paymentGateway;
|
private readonly IPaymentGatewayResolver _paymentGatewayResolver;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ISender _sender;
|
private readonly ISender _sender;
|
||||||
private readonly IUserPaymentLock _paymentLock;
|
private readonly IUserPaymentLock _paymentLock;
|
||||||
private readonly ILogger<VerifyUserPackagePurchasePaymentCommandHandler> _logger;
|
private readonly ILogger<VerifyUserPackagePurchasePaymentCommandHandler> _logger;
|
||||||
|
|
||||||
public VerifyUserPackagePurchasePaymentCommandHandler(
|
public VerifyUserPackagePurchasePaymentCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IPaymentGatewayService paymentGateway,
|
IPaymentGatewayResolver paymentGatewayResolver,
|
||||||
|
IConfiguration configuration,
|
||||||
ISender sender,
|
ISender sender,
|
||||||
IUserPaymentLock paymentLock,
|
IUserPaymentLock paymentLock,
|
||||||
ILogger<VerifyUserPackagePurchasePaymentCommandHandler> logger)
|
ILogger<VerifyUserPackagePurchasePaymentCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_paymentGateway = paymentGateway;
|
_paymentGatewayResolver = paymentGatewayResolver;
|
||||||
|
_configuration = configuration;
|
||||||
_sender = sender;
|
_sender = sender;
|
||||||
_paymentLock = paymentLock;
|
_paymentLock = paymentLock;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -105,7 +109,8 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
|
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
|
||||||
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
|
var gateway = _paymentGatewayResolver.Get(paymentTx?.GatewayProvider);
|
||||||
|
var verifyResult = await gateway.VerifyPaymentAsync(
|
||||||
request.Authority, request.Status, (decimal)amountInToman, ct);
|
request.Authority, request.Status, (decimal)amountInToman, ct);
|
||||||
|
|
||||||
if (!verifyResult.IsSuccess)
|
if (!verifyResult.IsSuccess)
|
||||||
@@ -193,17 +198,30 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
|
|||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
var discountMultiplier = purchase.Package?.DiscountMultiplier
|
var packagePrice = purchase.Package?.Price
|
||||||
?? throw new InvalidOperationException("پکیج سفارش یافت نشد");
|
?? throw new InvalidOperationException("پکیج سفارش یافت نشد");
|
||||||
var discountAmount = (long)(purchase.Amount * (double)discountMultiplier);
|
var discountMultiplier = purchase.Package.DiscountMultiplier;
|
||||||
wallet.Balance += purchase.Amount;
|
var discountAmount = (long)(packagePrice * (double)discountMultiplier);
|
||||||
|
|
||||||
|
var isTwentyCard = string.Equals(
|
||||||
|
paymentTx?.GatewayProvider, "20card", StringComparison.OrdinalIgnoreCase);
|
||||||
|
long balanceCredit = purchase.Amount;
|
||||||
|
if (isTwentyCard)
|
||||||
|
{
|
||||||
|
balanceCredit = _configuration.GetValue<long?>(
|
||||||
|
$"TwentyCard:Packages:{purchase.PackageId}:WalletCreditToman") ?? 0;
|
||||||
|
if (balanceCredit <= 0)
|
||||||
|
throw new InvalidOperationException("WalletCredit ۲۰کارت برای این پکیج پیکربندی نشده است");
|
||||||
|
}
|
||||||
|
|
||||||
|
wallet.Balance += balanceCredit;
|
||||||
wallet.DiscountBalance += discountAmount;
|
wallet.DiscountBalance += discountAmount;
|
||||||
|
|
||||||
_context.UserWalletHistories.Add(new UserWalletHistory
|
_context.UserWalletHistories.Add(new UserWalletHistory
|
||||||
{
|
{
|
||||||
WalletId = wallet.Id,
|
WalletId = wallet.Id,
|
||||||
CurrentBalance = wallet.Balance,
|
CurrentBalance = wallet.Balance,
|
||||||
ChangeValue = purchase.Amount,
|
ChangeValue = balanceCredit,
|
||||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||||
ChangeNerworkValue = 0,
|
ChangeNerworkValue = 0,
|
||||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||||
|
|||||||
+1
@@ -43,4 +43,5 @@ public class GetAllPaymentTransactionByFilterResponseModel
|
|||||||
public long? TransactionId { get; set; }
|
public long? TransactionId { get; set; }
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public DateTimeOffset Created { get; set; }
|
public DateTimeOffset Created { get; set; }
|
||||||
|
public string? UserFullName { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-8
@@ -19,12 +19,11 @@ public class GetAllPaymentTransactionByFilterQueryHandler
|
|||||||
.ApplyPaymentTransactionFilter(request.Filter)
|
.ApplyPaymentTransactionFilter(request.Filter)
|
||||||
.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
||||||
|
|
||||||
return new GetAllPaymentTransactionByFilterResponseDto
|
var models = await (
|
||||||
{
|
from x in query.PaginatedListAsync(paginationState: request.PaginationState)
|
||||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
join u in _context.Users.AsNoTracking() on x.UserId equals u.Id into ug
|
||||||
Models = await query
|
from u in ug.DefaultIfEmpty()
|
||||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
select new GetAllPaymentTransactionByFilterResponseModel
|
||||||
.Select(x => new GetAllPaymentTransactionByFilterResponseModel
|
|
||||||
{
|
{
|
||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
UserId = x.UserId,
|
UserId = x.UserId,
|
||||||
@@ -40,9 +39,23 @@ public class GetAllPaymentTransactionByFilterQueryHandler
|
|||||||
OrderId = x.OrderId,
|
OrderId = x.OrderId,
|
||||||
TransactionId = x.TransactionId,
|
TransactionId = x.TransactionId,
|
||||||
Description = x.Description,
|
Description = x.Description,
|
||||||
Created = x.Created
|
Created = x.Created,
|
||||||
|
UserFullName = u != null
|
||||||
|
? ((u.FirstName ?? "") + " " + (u.LastName ?? "")).Trim()
|
||||||
|
: null
|
||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken)
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
foreach (var model in models)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(model.UserFullName))
|
||||||
|
model.UserFullName = "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GetAllPaymentTransactionByFilterResponseDto
|
||||||
|
{
|
||||||
|
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||||
|
Models = models
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+90
-5
@@ -1,3 +1,5 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.TransactionsCQ.Queries.GetAllTransactionsByFilter;
|
namespace CMSMicroservice.Application.TransactionsCQ.Queries.GetAllTransactionsByFilter;
|
||||||
|
|
||||||
public class GetAllTransactionsByFilterQueryHandler : IRequestHandler<GetAllTransactionsByFilterQuery, GetAllTransactionsByFilterResponseDto>
|
public class GetAllTransactionsByFilterQueryHandler : IRequestHandler<GetAllTransactionsByFilterQuery, GetAllTransactionsByFilterResponseDto>
|
||||||
@@ -16,10 +18,7 @@ public class GetAllTransactionsByFilterQueryHandler : IRequestHandler<GetAllTran
|
|||||||
.ApplyTransactionFilter(request.Filter)
|
.ApplyTransactionFilter(request.Filter)
|
||||||
.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
||||||
|
|
||||||
return new GetAllTransactionsByFilterResponseDto
|
var models = await query
|
||||||
{
|
|
||||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
|
||||||
Models = await query
|
|
||||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||||
.Select(x => new GetAllTransactionsByFilterResponseModel
|
.Select(x => new GetAllTransactionsByFilterResponseModel
|
||||||
{
|
{
|
||||||
@@ -32,7 +31,93 @@ public class GetAllTransactionsByFilterQueryHandler : IRequestHandler<GetAllTran
|
|||||||
Type = x.Type,
|
Type = x.Type,
|
||||||
Created = x.Created
|
Created = x.Created
|
||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken)
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
await PopulateUserInfoAsync(models, cancellationToken);
|
||||||
|
|
||||||
|
return new GetAllTransactionsByFilterResponseDto
|
||||||
|
{
|
||||||
|
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||||
|
Models = models
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task PopulateUserInfoAsync(
|
||||||
|
List<GetAllTransactionsByFilterResponseModel> models,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (models.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var txIds = models.Select(m => m.Id).ToList();
|
||||||
|
var userByTx = new Dictionary<long, long>();
|
||||||
|
|
||||||
|
void Merge(IEnumerable<(long TxId, long UserId)> rows)
|
||||||
|
{
|
||||||
|
foreach (var (txId, userId) in rows)
|
||||||
|
{
|
||||||
|
if (!userByTx.ContainsKey(txId))
|
||||||
|
userByTx[txId] = userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority: PaymentTransactions → UserPackagePurchases → UserOrders → ManualPayments
|
||||||
|
var fromPt = await _context.PaymentTransactions.AsNoTracking()
|
||||||
|
.Where(x => x.TransactionId != null && txIds.Contains(x.TransactionId.Value) && x.UserId != null)
|
||||||
|
.Select(x => new { TxId = x.TransactionId!.Value, UserId = x.UserId!.Value })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
Merge(fromPt.Select(x => (x.TxId, x.UserId)));
|
||||||
|
|
||||||
|
var fromUpp = await _context.UserPackagePurchases.AsNoTracking()
|
||||||
|
.Where(x => x.TransactionId != null && txIds.Contains(x.TransactionId.Value))
|
||||||
|
.Select(x => new { TxId = x.TransactionId!.Value, UserId = x.UserId })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
Merge(fromUpp.Select(x => (x.TxId, x.UserId)));
|
||||||
|
|
||||||
|
var fromOrders = await _context.UserOrders.AsNoTracking()
|
||||||
|
.Where(x => x.TransactionId != null && txIds.Contains(x.TransactionId.Value))
|
||||||
|
.Select(x => new { TxId = x.TransactionId!.Value, UserId = x.UserId })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
Merge(fromOrders.Select(x => (x.TxId, x.UserId)));
|
||||||
|
|
||||||
|
var fromManual = await _context.ManualPayments.AsNoTracking()
|
||||||
|
.Where(x => x.TransactionId != null && txIds.Contains(x.TransactionId.Value))
|
||||||
|
.Select(x => new { TxId = x.TransactionId!.Value, UserId = x.UserId })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
Merge(fromManual.Select(x => (x.TxId, x.UserId)));
|
||||||
|
|
||||||
|
if (userByTx.Count == 0)
|
||||||
|
{
|
||||||
|
foreach (var model in models)
|
||||||
|
model.UserFullName = "—";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userIds = userByTx.Values.Distinct().ToList();
|
||||||
|
var users = await _context.Users.AsNoTracking()
|
||||||
|
.Where(u => userIds.Contains(u.Id))
|
||||||
|
.Select(u => new { u.Id, u.FirstName, u.LastName, u.Mobile })
|
||||||
|
.ToDictionaryAsync(u => u.Id, cancellationToken);
|
||||||
|
|
||||||
|
foreach (var model in models)
|
||||||
|
{
|
||||||
|
if (!userByTx.TryGetValue(model.Id, out var userId))
|
||||||
|
{
|
||||||
|
model.UserFullName = "—";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
model.UserId = userId;
|
||||||
|
if (users.TryGetValue(userId, out var user))
|
||||||
|
{
|
||||||
|
model.Mobile = user.Mobile;
|
||||||
|
var name = UserDisplayName.From(user.FirstName, user.LastName, user.Mobile, user.Id);
|
||||||
|
model.UserFullName = string.IsNullOrWhiteSpace(name) ? "—" : name;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
model.UserFullName = "—";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -25,4 +25,7 @@ public class GetAllTransactionsByFilterResponseDto
|
|||||||
//
|
//
|
||||||
public TransactionType Type { get; set; }
|
public TransactionType Type { get; set; }
|
||||||
public DateTime Created { get; set; }
|
public DateTime Created { get; set; }
|
||||||
|
public long? UserId { get; set; }
|
||||||
|
public string? UserFullName { get; set; }
|
||||||
|
public string? Mobile { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -9,5 +9,7 @@ public record VerifyOtpTokenCommand : IRequest<VerifyOtpTokenResponseDto>
|
|||||||
public string Code { get; init; }
|
public string Code { get; init; }
|
||||||
//کد معرف والد
|
//کد معرف والد
|
||||||
public string? ParentReferralCode { get; init; }
|
public string? ParentReferralCode { get; init; }
|
||||||
|
//شماره موبایل دعوتکننده (اختیاری)
|
||||||
|
public string? InviterMobile { get; init; }
|
||||||
|
|
||||||
}
|
}
|
||||||
+37
@@ -118,6 +118,34 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
|
|||||||
else
|
else
|
||||||
return new VerifyOtpTokenResponseDto { Success = false, Message = "ظرفیت معرف تکمیل است!!" };
|
return new VerifyOtpTokenResponseDto { Success = false, Message = "ظرفیت معرف تکمیل است!!" };
|
||||||
|
|
||||||
|
// ── دعوتکننده (اختیاری؛ اعتبارسنجی سخت قبل از ایجاد کاربر) ──
|
||||||
|
User? inviter = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.InviterMobile))
|
||||||
|
{
|
||||||
|
var inviterMobile = request.InviterMobile.NormalizeIranMobile();
|
||||||
|
if (inviterMobile == mobile)
|
||||||
|
{
|
||||||
|
return new VerifyOtpTokenResponseDto
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "شماره موبایل دعوتکننده نمیتواند با شماره خودتان یکسان باشد."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inviter = await _context.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(u => u.Mobile == inviterMobile, cancellationToken);
|
||||||
|
|
||||||
|
if (inviter == null)
|
||||||
|
{
|
||||||
|
return new VerifyOtpTokenResponseDto
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "دعوتکننده با این شماره موبایل یافت نشد."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ایجاد کاربر جدید
|
// ایجاد کاربر جدید
|
||||||
user = new User
|
user = new User
|
||||||
{
|
{
|
||||||
@@ -134,6 +162,15 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
|
|||||||
user.AddDomainEvent(new CreateNewUserEvent(user));
|
user.AddDomainEvent(new CreateNewUserEvent(user));
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (inviter != null)
|
||||||
|
{
|
||||||
|
await _context.UserInvitations.AddAsync(new UserInvitation
|
||||||
|
{
|
||||||
|
InviterUserId = inviter.Id,
|
||||||
|
InviteeUserId = user.Id
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
// ایجاد نقش کاربری
|
// ایجاد نقش کاربری
|
||||||
var userRole = new UserRole { UserId = user.Id, RoleId = 1 };
|
var userRole = new UserRole { UserId = user.Id, RoleId = 1 };
|
||||||
await _context.UserRoles.AddAsync(userRole, cancellationToken);
|
await _context.UserRoles.AddAsync(userRole, cancellationToken);
|
||||||
|
|||||||
+28
-3
@@ -34,14 +34,39 @@ public class GetAllUserByFilterQueryHandler : IRequestHandler<GetAllUserByFilter
|
|||||||
.Where(x => request.Filter.SmsNotifications == null || x.SmsNotifications == request.Filter.SmsNotifications)
|
.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.EmailNotifications == null || x.EmailNotifications == request.Filter.EmailNotifications)
|
||||||
.Where(x => request.Filter.PushNotifications == null || x.PushNotifications == request.Filter.PushNotifications)
|
.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
|
return new GetAllUserByFilterResponseDto
|
||||||
{
|
{
|
||||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||||
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
|
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
|
||||||
.ProjectToType<GetAllUserByFilterResponseModel>().ToListAsync(cancellationToken)
|
.Select(x => new GetAllUserByFilterResponseModel
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
FirstName = x.FirstName,
|
||||||
|
LastName = x.LastName,
|
||||||
|
Mobile = x.Mobile,
|
||||||
|
NationalCode = x.NationalCode,
|
||||||
|
AvatarPath = x.AvatarPath,
|
||||||
|
NetworkParentId = x.NetworkParentId,
|
||||||
|
ReferralCode = x.ReferralCode,
|
||||||
|
IsMobileVerified = x.IsMobileVerified,
|
||||||
|
MobileVerifiedAt = x.MobileVerifiedAt,
|
||||||
|
EmailNotifications = x.EmailNotifications,
|
||||||
|
SmsNotifications = x.SmsNotifications,
|
||||||
|
PushNotifications = x.PushNotifications,
|
||||||
|
BirthDate = x.BirthDate,
|
||||||
|
DefaultAddress = x.UserAddresses
|
||||||
|
.Where(a => a.IsDefault && !a.IsDeleted)
|
||||||
|
.Select(a => a.Address)
|
||||||
|
.FirstOrDefault() ?? string.Empty,
|
||||||
|
DefaultPostalCode = x.UserAddresses
|
||||||
|
.Where(a => a.IsDefault && !a.IsDeleted)
|
||||||
|
.Select(a => a.PostalCode)
|
||||||
|
.FirstOrDefault() ?? string.Empty
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -36,4 +36,8 @@ public class GetAllUserByFilterResponseDto
|
|||||||
public bool PushNotifications { get; set; }
|
public bool PushNotifications { get; set; }
|
||||||
//تاریخ تولد
|
//تاریخ تولد
|
||||||
public DateTime? BirthDate { get; set; }
|
public DateTime? BirthDate { get; set; }
|
||||||
|
//آدرس پیشفرض
|
||||||
|
public string DefaultAddress { get; set; } = string.Empty;
|
||||||
|
//کدپستی پیشفرض
|
||||||
|
public string DefaultPostalCode { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.UserCQ.Queries.GetInvitationsByFilter;
|
||||||
|
|
||||||
|
public class GetInvitationsByFilterQuery : IRequest<GetInvitationsByFilterResult>
|
||||||
|
{
|
||||||
|
public PaginationState? PaginationState { get; set; }
|
||||||
|
public string? SortBy { get; set; }
|
||||||
|
public GetInvitationsByFilterFilterDto? Filter { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetInvitationsByFilterFilterDto
|
||||||
|
{
|
||||||
|
public string? InviterMobile { get; set; }
|
||||||
|
public string? InviterFirstName { get; set; }
|
||||||
|
public string? InviterLastName { get; set; }
|
||||||
|
public string? InviteeMobile { get; set; }
|
||||||
|
public string? InviteeFirstName { get; set; }
|
||||||
|
public string? InviteeLastName { get; set; }
|
||||||
|
public bool? InviteeClubActive { get; set; }
|
||||||
|
public DateTime? CreatedFrom { get; set; }
|
||||||
|
public DateTime? CreatedTo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetInvitationsByFilterResult
|
||||||
|
{
|
||||||
|
public MetaData MetaData { get; set; } = new();
|
||||||
|
public List<InvitationReportDto> Models { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InvitationReportDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public long InviterUserId { get; set; }
|
||||||
|
public string? InviterFirstName { get; set; }
|
||||||
|
public string? InviterLastName { get; set; }
|
||||||
|
public string InviterMobile { get; set; } = string.Empty;
|
||||||
|
public long InviteeUserId { get; set; }
|
||||||
|
public string? InviteeFirstName { get; set; }
|
||||||
|
public string? InviteeLastName { get; set; }
|
||||||
|
public string InviteeMobile { get; set; } = string.Empty;
|
||||||
|
public DateTime InviteeRegisteredAt { get; set; }
|
||||||
|
public bool InviteeClubActive { get; set; }
|
||||||
|
public DateTime InvitedAt { get; set; }
|
||||||
|
}
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.UserCQ.Queries.GetInvitationsByFilter;
|
||||||
|
|
||||||
|
public class GetInvitationsByFilterQueryHandler
|
||||||
|
: IRequestHandler<GetInvitationsByFilterQuery, GetInvitationsByFilterResult>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetInvitationsByFilterQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetInvitationsByFilterResult> Handle(
|
||||||
|
GetInvitationsByFilterQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var filter = request.Filter ?? new GetInvitationsByFilterFilterDto();
|
||||||
|
|
||||||
|
var query = _context.UserInvitations
|
||||||
|
.AsNoTracking()
|
||||||
|
.Select(x => new InvitationReportDto
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
InviterUserId = x.InviterUserId,
|
||||||
|
InviterFirstName = x.Inviter.FirstName,
|
||||||
|
InviterLastName = x.Inviter.LastName,
|
||||||
|
InviterMobile = x.Inviter.Mobile,
|
||||||
|
InviteeUserId = x.InviteeUserId,
|
||||||
|
InviteeFirstName = x.Invitee.FirstName,
|
||||||
|
InviteeLastName = x.Invitee.LastName,
|
||||||
|
InviteeMobile = x.Invitee.Mobile,
|
||||||
|
InviteeRegisteredAt = x.Invitee.Created,
|
||||||
|
InviteeClubActive = x.Invitee.ClubMembership != null && x.Invitee.ClubMembership.IsActive,
|
||||||
|
InvitedAt = x.Created
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.InviterMobile))
|
||||||
|
query = query.Where(x => x.InviterMobile.Contains(filter.InviterMobile));
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.InviterFirstName))
|
||||||
|
query = query.Where(x => x.InviterFirstName != null && x.InviterFirstName.Contains(filter.InviterFirstName));
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.InviterLastName))
|
||||||
|
query = query.Where(x => x.InviterLastName != null && x.InviterLastName.Contains(filter.InviterLastName));
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.InviteeMobile))
|
||||||
|
query = query.Where(x => x.InviteeMobile.Contains(filter.InviteeMobile));
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.InviteeFirstName))
|
||||||
|
query = query.Where(x => x.InviteeFirstName != null && x.InviteeFirstName.Contains(filter.InviteeFirstName));
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.InviteeLastName))
|
||||||
|
query = query.Where(x => x.InviteeLastName != null && x.InviteeLastName.Contains(filter.InviteeLastName));
|
||||||
|
if (filter.InviteeClubActive.HasValue)
|
||||||
|
query = query.Where(x => x.InviteeClubActive == filter.InviteeClubActive.Value);
|
||||||
|
if (filter.CreatedFrom.HasValue)
|
||||||
|
query = query.Where(x => x.InvitedAt >= filter.CreatedFrom.Value);
|
||||||
|
if (filter.CreatedTo.HasValue)
|
||||||
|
query = query.Where(x => x.InvitedAt <= filter.CreatedTo.Value);
|
||||||
|
|
||||||
|
// پیشفرض: جدیدترین
|
||||||
|
query = (request.SortBy?.Trim().ToLowerInvariant()) switch
|
||||||
|
{
|
||||||
|
"oldest" or "created_asc" => query.OrderBy(x => x.InvitedAt),
|
||||||
|
_ => query.OrderByDescending(x => x.InvitedAt)
|
||||||
|
};
|
||||||
|
|
||||||
|
var paginationState = request.PaginationState ?? new PaginationState { PageNumber = 1, PageSize = 20 };
|
||||||
|
if (paginationState.PageNumber < 1) paginationState.PageNumber = 1;
|
||||||
|
if (paginationState.PageSize < 1) paginationState.PageSize = 20;
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
var models = await query
|
||||||
|
.Skip((paginationState.PageNumber - 1) * paginationState.PageSize)
|
||||||
|
.Take(paginationState.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var totalPage = totalCount == 0
|
||||||
|
? 0
|
||||||
|
: (int)Math.Ceiling((double)totalCount / paginationState.PageSize);
|
||||||
|
|
||||||
|
return new GetInvitationsByFilterResult
|
||||||
|
{
|
||||||
|
MetaData = new MetaData
|
||||||
|
{
|
||||||
|
TotalCount = totalCount,
|
||||||
|
CurrentPage = paginationState.PageNumber,
|
||||||
|
PageSize = paginationState.PageSize,
|
||||||
|
TotalPage = totalPage,
|
||||||
|
HasPrevious = paginationState.PageNumber > 1,
|
||||||
|
HasNext = paginationState.PageNumber < totalPage
|
||||||
|
},
|
||||||
|
Models = models
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.UserCQ.Queries.GetMyInvitees;
|
||||||
|
|
||||||
|
public class GetMyInviteesQuery : IRequest<GetMyInviteesResult>
|
||||||
|
{
|
||||||
|
public long UserId { get; set; }
|
||||||
|
public PaginationState? PaginationState { get; set; }
|
||||||
|
/// <summary>ALL | ACTIVE | INACTIVE — ClubMembership.IsActive</summary>
|
||||||
|
public string? ClubStatusFilter { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetMyInviteesResult
|
||||||
|
{
|
||||||
|
public MetaData MetaData { get; set; } = new();
|
||||||
|
public List<InviteeDto> Invitees { get; set; } = new();
|
||||||
|
public InviteeStatsDto Stats { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InviteeDto
|
||||||
|
{
|
||||||
|
public long UserId { get; set; }
|
||||||
|
public string? FirstName { get; set; }
|
||||||
|
public string? LastName { get; set; }
|
||||||
|
public string Mobile { get; set; } = string.Empty;
|
||||||
|
public DateTime RegisteredAt { get; set; }
|
||||||
|
public bool IsClubActive { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InviteeStatsDto
|
||||||
|
{
|
||||||
|
public int TotalInvitees { get; set; }
|
||||||
|
public int ClubActiveCount { get; set; }
|
||||||
|
public int ClubInactiveCount { get; set; }
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.UserCQ.Queries.GetMyInvitees;
|
||||||
|
|
||||||
|
public class GetMyInviteesQueryHandler : IRequestHandler<GetMyInviteesQuery, GetMyInviteesResult>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
|
public GetMyInviteesQueryHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetMyInviteesResult> Handle(GetMyInviteesQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userId = request.UserId == 0
|
||||||
|
? (long.TryParse(_currentUser.UserId, out var id) ? id : 0)
|
||||||
|
: request.UserId;
|
||||||
|
|
||||||
|
if (userId == 0)
|
||||||
|
throw new UnauthorizedAccessException("User ID not found in JWT token");
|
||||||
|
|
||||||
|
var baseQuery = _context.UserInvitations
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.InviterUserId == userId)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.InviteeUserId,
|
||||||
|
InviteeFirstName = x.Invitee.FirstName,
|
||||||
|
InviteeLastName = x.Invitee.LastName,
|
||||||
|
InviteeMobile = x.Invitee.Mobile,
|
||||||
|
RegisteredAt = x.Invitee.Created,
|
||||||
|
IsClubActive = x.Invitee.ClubMembership != null && x.Invitee.ClubMembership.IsActive,
|
||||||
|
InvitedAt = x.Created
|
||||||
|
});
|
||||||
|
|
||||||
|
var all = await baseQuery.ToListAsync(cancellationToken);
|
||||||
|
var total = all.Count;
|
||||||
|
var clubActive = all.Count(x => x.IsClubActive);
|
||||||
|
var clubInactive = total - clubActive;
|
||||||
|
|
||||||
|
var filtered = all.AsEnumerable();
|
||||||
|
var status = request.ClubStatusFilter?.Trim().ToUpperInvariant();
|
||||||
|
if (status == "ACTIVE")
|
||||||
|
filtered = filtered.Where(x => x.IsClubActive);
|
||||||
|
else if (status == "INACTIVE")
|
||||||
|
filtered = filtered.Where(x => !x.IsClubActive);
|
||||||
|
|
||||||
|
var ordered = filtered.OrderByDescending(x => x.InvitedAt).ToList();
|
||||||
|
|
||||||
|
var paginationState = request.PaginationState ?? new PaginationState { PageNumber = 1, PageSize = 10 };
|
||||||
|
if (paginationState.PageNumber < 1) paginationState.PageNumber = 1;
|
||||||
|
if (paginationState.PageSize < 1) paginationState.PageSize = 10;
|
||||||
|
|
||||||
|
var totalCount = ordered.Count;
|
||||||
|
var pageItems = ordered
|
||||||
|
.Skip((paginationState.PageNumber - 1) * paginationState.PageSize)
|
||||||
|
.Take(paginationState.PageSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var totalPage = totalCount == 0
|
||||||
|
? 0
|
||||||
|
: (int)Math.Ceiling((double)totalCount / paginationState.PageSize);
|
||||||
|
|
||||||
|
return new GetMyInviteesResult
|
||||||
|
{
|
||||||
|
MetaData = new MetaData
|
||||||
|
{
|
||||||
|
TotalCount = totalCount,
|
||||||
|
CurrentPage = paginationState.PageNumber,
|
||||||
|
PageSize = paginationState.PageSize,
|
||||||
|
TotalPage = totalPage,
|
||||||
|
HasPrevious = paginationState.PageNumber > 1,
|
||||||
|
HasNext = paginationState.PageNumber < totalPage
|
||||||
|
},
|
||||||
|
Stats = new InviteeStatsDto
|
||||||
|
{
|
||||||
|
TotalInvitees = total,
|
||||||
|
ClubActiveCount = clubActive,
|
||||||
|
ClubInactiveCount = clubInactive
|
||||||
|
},
|
||||||
|
Invitees = pageItems.Select(x => new InviteeDto
|
||||||
|
{
|
||||||
|
UserId = x.InviteeUserId,
|
||||||
|
FirstName = x.InviteeFirstName,
|
||||||
|
LastName = x.InviteeLastName,
|
||||||
|
Mobile = x.InviteeMobile,
|
||||||
|
RegisteredAt = x.RegisteredAt,
|
||||||
|
IsClubActive = x.IsClubActive
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
using CMSMicroservice.Application.Common.Exceptions;
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities;
|
using CMSMicroservice.Domain.Entities;
|
||||||
@@ -50,8 +51,10 @@ public class GetCustomerOrderQueryHandler : IRequestHandler<GetCustomerOrderQuer
|
|||||||
DeliveryStatus = order.DeliveryStatus,
|
DeliveryStatus = order.DeliveryStatus,
|
||||||
TrackingCode = order.TrackingCode ?? "",
|
TrackingCode = order.TrackingCode ?? "",
|
||||||
DeliveryDescription = order.DeliveryDescription ?? "",
|
DeliveryDescription = order.DeliveryDescription ?? "",
|
||||||
UserFullName = $"{order.User?.FirstName ?? ""} {order.User?.LastName ?? ""}".Trim(),
|
UserFullName = UserDisplayName.From(order.User),
|
||||||
UserNationalCode = order.User?.NationalCode ?? "",
|
UserNationalCode = order.User?.NationalCode ?? "",
|
||||||
|
PostalCode = order.UserAddress?.PostalCode ?? "",
|
||||||
|
UserMobile = order.User?.Mobile ?? "",
|
||||||
VatAmount = order.OrderVAT?.VATAmount ?? 0,
|
VatAmount = order.OrderVAT?.VATAmount ?? 0,
|
||||||
VatPercentage = order.OrderVAT != null ? (double)order.OrderVAT.VATRate * 100 : 0,
|
VatPercentage = order.OrderVAT != null ? (double)order.OrderVAT.VATRate * 100 : 0,
|
||||||
FactorDetails = order.FactorDetails?.Select(fd => new FactorDetailDto
|
FactorDetails = order.FactorDetails?.Select(fd => new FactorDetailDto
|
||||||
|
|||||||
+2
@@ -20,6 +20,8 @@ public class GetCustomerOrderResponseDto
|
|||||||
public string DeliveryDescription { get; set; }
|
public string DeliveryDescription { get; set; }
|
||||||
public string UserFullName { get; set; }
|
public string UserFullName { get; set; }
|
||||||
public string UserNationalCode { 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 long VatAmount { get; set; }
|
||||||
public double VatPercentage { get; set; }
|
public double VatPercentage { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -113,6 +113,7 @@ public class GetCustomerOrderHistoryQueryHandler : IRequestHandler<GetCustomerOr
|
|||||||
DeliveryStatus.Delivered => 4,
|
DeliveryStatus.Delivered => 4,
|
||||||
DeliveryStatus.Cancelled => 5,
|
DeliveryStatus.Cancelled => 5,
|
||||||
DeliveryStatus.Returned => 6,
|
DeliveryStatus.Returned => 6,
|
||||||
|
DeliveryStatus.ReadyForOfficePickup => 7,
|
||||||
_ => 0
|
_ => 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -127,6 +128,7 @@ public class GetCustomerOrderHistoryQueryHandler : IRequestHandler<GetCustomerOr
|
|||||||
DeliveryStatus.Delivered => "تحویل داده شد",
|
DeliveryStatus.Delivered => "تحویل داده شد",
|
||||||
DeliveryStatus.Cancelled => "لغو شده",
|
DeliveryStatus.Cancelled => "لغو شده",
|
||||||
DeliveryStatus.Returned => "مرجوع شده",
|
DeliveryStatus.Returned => "مرجوع شده",
|
||||||
|
DeliveryStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
|
||||||
_ => "نامشخص"
|
_ => "نامشخص"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
using CMSMicroservice.Application.Common.Extensions;
|
using CMSMicroservice.Application.Common.Extensions;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Application.Common.Models;
|
using CMSMicroservice.Application.Common.Models;
|
||||||
@@ -72,7 +73,7 @@ public class GetCustomerOrdersQueryHandler : IRequestHandler<GetCustomerOrdersQu
|
|||||||
DeliveryStatus = order.DeliveryStatus,
|
DeliveryStatus = order.DeliveryStatus,
|
||||||
TrackingCode = order.TrackingCode ?? "",
|
TrackingCode = order.TrackingCode ?? "",
|
||||||
DeliveryDescription = order.DeliveryDescription ?? "",
|
DeliveryDescription = order.DeliveryDescription ?? "",
|
||||||
UserFullName = $"{order.User?.FirstName ?? ""} {order.User?.LastName ?? ""}".Trim(),
|
UserFullName = UserDisplayName.From(order.User),
|
||||||
UserNationalCode = order.User?.NationalCode ?? "",
|
UserNationalCode = order.User?.NationalCode ?? "",
|
||||||
VatAmount = order.OrderVAT?.VATAmount ?? 0,
|
VatAmount = order.OrderVAT?.VATAmount ?? 0,
|
||||||
VatPercentage = order.OrderVAT != null ? (double)order.OrderVAT.VATRate * 100 : 0,
|
VatPercentage = order.OrderVAT != null ? (double)order.OrderVAT.VATRate * 100 : 0,
|
||||||
|
|||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace CMSMicroservice.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// رابطه دعوتکننده ↔ دعوتشونده (جدا از معرف شبکه / NetworkParent).
|
||||||
|
/// کد دعوت = شماره موبایل دعوتکننده.
|
||||||
|
/// </summary>
|
||||||
|
public class UserInvitation : BaseAuditableEntity
|
||||||
|
{
|
||||||
|
/// <summary>کاربری که دعوت کرده</summary>
|
||||||
|
public long InviterUserId { get; set; }
|
||||||
|
|
||||||
|
public virtual User Inviter { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>کاربری که دعوت شده (حداکثر یک دعوتکننده)</summary>
|
||||||
|
public long InviteeUserId { get; set; }
|
||||||
|
|
||||||
|
public virtual User Invitee { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -15,5 +15,7 @@ public enum DeliveryStatus
|
|||||||
Returned = 4,
|
Returned = 4,
|
||||||
// لغو شده
|
// لغو شده
|
||||||
Cancelled = 5,
|
Cancelled = 5,
|
||||||
|
// آماده تحویل حضوری در دفتر
|
||||||
|
ReadyForOfficePickup = 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,5 +18,10 @@ public enum PackagePurchaseMethod
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// از طریق پرداخت مستقیم درگاه بانکی
|
/// از طریق پرداخت مستقیم درگاه بانکی
|
||||||
/// </summary>
|
/// </summary>
|
||||||
DirectPurchase = 2
|
DirectPurchase = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ثبت دستی توسط ادمین
|
||||||
|
/// </summary>
|
||||||
|
Manual = 3
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,20 +77,24 @@ public static class ConfigureServices
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payment Gateway Service - Multi-Provider Architecture
|
// Payment Gateway — default provider + multi-gateway resolver (ZarinPal + TwentyCard coexist)
|
||||||
// پشتیبانی از درگاههای مختلف: ZarinPal, Daya, Mock
|
|
||||||
var paymentProvider = configuration.GetValue<string>("PaymentProvider", "Mock")?.ToLowerInvariant();
|
var paymentProvider = configuration.GetValue<string>("PaymentProvider", "Mock")?.ToLowerInvariant();
|
||||||
|
|
||||||
|
services.AddHttpClient<ZarinPalPaymentService>()
|
||||||
|
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||||
|
services.AddHttpClient<TwentyCardPaymentService>()
|
||||||
|
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||||
|
|
||||||
switch (paymentProvider)
|
switch (paymentProvider)
|
||||||
{
|
{
|
||||||
case "zarinpal":
|
case "zarinpal":
|
||||||
services.AddHttpClient<IPaymentGatewayService, ZarinPalPaymentService>()
|
services.AddScoped<IPaymentGatewayService>(sp => sp.GetRequiredService<ZarinPalPaymentService>());
|
||||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "daya":
|
case "daya":
|
||||||
services.AddHttpClient<IPaymentGatewayService, DayaPaymentService>()
|
services.AddHttpClient<DayaPaymentService>()
|
||||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||||
|
services.AddScoped<IPaymentGatewayService>(sp => sp.GetRequiredService<DayaPaymentService>());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "mock":
|
case "mock":
|
||||||
@@ -99,6 +103,8 @@ public static class ConfigureServices
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
services.AddScoped<IPaymentGatewayResolver, PaymentGatewayResolver>();
|
||||||
|
|
||||||
services.AddScoped<IApplicationDbContext>(p => p.GetRequiredService<ApplicationDbContext>());
|
services.AddScoped<IApplicationDbContext>(p => p.GetRequiredService<ApplicationDbContext>());
|
||||||
|
|
||||||
// Week Definition Repository - کش در حافظه برای هفتهها (Singleton برای کش، با IServiceScopeFactory برای دسترسی به DbContext)
|
// Week Definition Repository - کش در حافظه برای هفتهها (Singleton برای کش، با IServiceScopeFactory برای دسترسی به DbContext)
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
|
|||||||
public DbSet<Product> Products => Set<Product>();
|
public DbSet<Product> Products => Set<Product>();
|
||||||
public DbSet<ProductImage> ProductImages => Set<ProductImage>();
|
public DbSet<ProductImage> ProductImages => Set<ProductImage>();
|
||||||
public DbSet<User> Users => Set<User>();
|
public DbSet<User> Users => Set<User>();
|
||||||
|
public DbSet<UserInvitation> UserInvitations => Set<UserInvitation>();
|
||||||
public DbSet<OtpToken> OtpTokens => Set<OtpToken>();
|
public DbSet<OtpToken> OtpTokens => Set<OtpToken>();
|
||||||
public DbSet<Contract> Contracts => Set<Contract>();
|
public DbSet<Contract> Contracts => Set<Contract>();
|
||||||
public DbSet<UserContract> UserContracts => Set<UserContract>();
|
public DbSet<UserContract> UserContracts => Set<UserContract>();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
|
|||||||
builder.Property(entity => entity.Id).UseIdentityColumn();
|
builder.Property(entity => entity.Id).UseIdentityColumn();
|
||||||
builder.Property(entity => entity.FirstName).IsRequired(false);
|
builder.Property(entity => entity.FirstName).IsRequired(false);
|
||||||
builder.Property(entity => entity.LastName).IsRequired(false);
|
builder.Property(entity => entity.LastName).IsRequired(false);
|
||||||
builder.Property(entity => entity.Mobile).IsRequired(true);
|
builder.Property(entity => entity.Mobile).IsRequired(true).HasMaxLength(20);
|
||||||
builder.Property(entity => entity.NationalCode).IsRequired(false);
|
builder.Property(entity => entity.NationalCode).IsRequired(false);
|
||||||
builder.Property(entity => entity.AvatarPath).IsRequired(false);
|
builder.Property(entity => entity.AvatarPath).IsRequired(false);
|
||||||
builder.Property(entity => entity.ReferralCode).IsRequired(true);
|
builder.Property(entity => entity.ReferralCode).IsRequired(true);
|
||||||
@@ -48,5 +48,10 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
|
|||||||
builder.HasIndex(e => e.LegPosition)
|
builder.HasIndex(e => e.LegPosition)
|
||||||
.HasDatabaseName("IX_User_LegPosition");
|
.HasDatabaseName("IX_User_LegPosition");
|
||||||
|
|
||||||
|
// موبایل یکتا برای کاربران فعال (lookup دعوتکننده)
|
||||||
|
builder.HasIndex(e => e.Mobile)
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("[IsDeleted] = 0")
|
||||||
|
.HasDatabaseName("IX_User_Mobile_Active");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
|
||||||
|
|
||||||
|
public class UserInvitationConfiguration : IEntityTypeConfiguration<UserInvitation>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UserInvitation> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("UserInvitations");
|
||||||
|
builder.HasQueryFilter(p => !p.IsDeleted);
|
||||||
|
builder.Ignore(entity => entity.DomainEvents);
|
||||||
|
|
||||||
|
builder.HasKey(entity => entity.Id);
|
||||||
|
builder.Property(entity => entity.Id).UseIdentityColumn();
|
||||||
|
|
||||||
|
builder.Property(entity => entity.InviterUserId).IsRequired();
|
||||||
|
builder.Property(entity => entity.InviteeUserId).IsRequired();
|
||||||
|
|
||||||
|
builder.HasOne(entity => entity.Inviter)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(entity => entity.InviterUserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
builder.HasOne(entity => entity.Invitee)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(entity => entity.InviteeUserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
builder.HasIndex(e => e.InviteeUserId)
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_UserInvitations_InviteeUserId");
|
||||||
|
|
||||||
|
builder.HasIndex(e => new { e.InviterUserId, e.Created })
|
||||||
|
.HasDatabaseName("IX_UserInvitations_InviterUserId_Created");
|
||||||
|
}
|
||||||
|
}
|
||||||
+5269
File diff suppressed because it is too large
Load Diff
+103
@@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUserInvitations : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Mobile",
|
||||||
|
schema: "CMS",
|
||||||
|
table: "Users",
|
||||||
|
type: "nvarchar(20)",
|
||||||
|
maxLength: 20,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "nvarchar(max)");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserInvitations",
|
||||||
|
schema: "CMS",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
InviterUserId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
InviteeUserId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UserInvitations", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserInvitations_Users_InviteeUserId",
|
||||||
|
column: x => x.InviteeUserId,
|
||||||
|
principalSchema: "CMS",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserInvitations_Users_InviterUserId",
|
||||||
|
column: x => x.InviterUserId,
|
||||||
|
principalSchema: "CMS",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_User_Mobile_Active",
|
||||||
|
schema: "CMS",
|
||||||
|
table: "Users",
|
||||||
|
column: "Mobile",
|
||||||
|
unique: true,
|
||||||
|
filter: "[IsDeleted] = 0");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserInvitations_InviteeUserId",
|
||||||
|
schema: "CMS",
|
||||||
|
table: "UserInvitations",
|
||||||
|
column: "InviteeUserId",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserInvitations_InviterUserId_Created",
|
||||||
|
schema: "CMS",
|
||||||
|
table: "UserInvitations",
|
||||||
|
columns: new[] { "InviterUserId", "Created" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserInvitations",
|
||||||
|
schema: "CMS");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_User_Mobile_Active",
|
||||||
|
schema: "CMS",
|
||||||
|
table: "Users");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Mobile",
|
||||||
|
schema: "CMS",
|
||||||
|
table: "Users",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "nvarchar(20)",
|
||||||
|
oldMaxLength: 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+66
-1
@@ -3574,7 +3574,7 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
|||||||
|
|
||||||
b.Property<string>("Mobile")
|
b.Property<string>("Mobile")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
b.Property<DateTime?>("MobileVerifiedAt")
|
b.Property<DateTime?>("MobileVerifiedAt")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
@@ -3606,6 +3606,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
|||||||
b.HasIndex("LegPosition")
|
b.HasIndex("LegPosition")
|
||||||
.HasDatabaseName("IX_User_LegPosition");
|
.HasDatabaseName("IX_User_LegPosition");
|
||||||
|
|
||||||
|
b.HasIndex("Mobile")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_User_Mobile_Active")
|
||||||
|
.HasFilter("[IsDeleted] = 0");
|
||||||
|
|
||||||
b.HasIndex("NetworkParentId")
|
b.HasIndex("NetworkParentId")
|
||||||
.HasDatabaseName("IX_User_NetworkParentId");
|
.HasDatabaseName("IX_User_NetworkParentId");
|
||||||
|
|
||||||
@@ -3750,6 +3755,47 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
|||||||
b.ToTable("UserContracts", "CMS");
|
b.ToTable("UserContracts", "CMS");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserInvitation", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<long>("InviteeUserId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("InviterUserId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastModified")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("LastModifiedBy")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InviteeUserId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_UserInvitations_InviteeUserId");
|
||||||
|
|
||||||
|
b.HasIndex("InviterUserId", "Created")
|
||||||
|
.HasDatabaseName("IX_UserInvitations_InviterUserId_Created");
|
||||||
|
|
||||||
|
b.ToTable("UserInvitations", "CMS");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b =>
|
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -4871,6 +4917,25 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
|||||||
b.Navigation("User");
|
b.Navigation("User");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserInvitation", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CMSMicroservice.Domain.Entities.User", "Invitee")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("InviteeUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CMSMicroservice.Domain.Entities.User", "Inviter")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("InviterUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Invitee");
|
||||||
|
|
||||||
|
b.Navigation("Inviter");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b =>
|
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
|
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities;
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -13,10 +14,12 @@ namespace CMSMicroservice.Infrastructure.Services;
|
|||||||
public class GenerateJwtTokenService : IGenerateJwtToken
|
public class GenerateJwtTokenService : IGenerateJwtToken
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
public GenerateJwtTokenService(IConfiguration configuration)
|
public GenerateJwtTokenService(IConfiguration configuration, IApplicationDbContext context)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
|
_context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GenerateJwtToken(User user, int? expiryDays = null)
|
public async Task<string> GenerateJwtToken(User user, int? expiryDays = null)
|
||||||
@@ -58,6 +61,10 @@ public class GenerateJwtTokenService : IGenerateJwtToken
|
|||||||
claims.Add(new Claim("HasPurchasedPackage",
|
claims.Add(new Claim("HasPurchasedPackage",
|
||||||
(user.PackagePurchaseMethod != PackagePurchaseMethod.None).ToString()));
|
(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())
|
if (user.UserRoles != null && user.UserRoles.Any())
|
||||||
{
|
{
|
||||||
foreach (var userRole in user.UserRoles)
|
foreach (var userRole in user.UserRoles)
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Infrastructure.Services.Payment;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Services.Payment;
|
||||||
|
|
||||||
|
public class PaymentGatewayResolver : IPaymentGatewayResolver
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public PaymentGatewayResolver(IServiceProvider serviceProvider, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPaymentGatewayService Get(string? provider)
|
||||||
|
{
|
||||||
|
var key = Normalize(provider)
|
||||||
|
?? Normalize(_configuration["PaymentProvider"])
|
||||||
|
?? "zarinpal";
|
||||||
|
|
||||||
|
return key switch
|
||||||
|
{
|
||||||
|
"20card" or "twentycard" => _serviceProvider.GetRequiredService<TwentyCardPaymentService>(),
|
||||||
|
"zarinpal" => _serviceProvider.GetRequiredService<ZarinPalPaymentService>(),
|
||||||
|
_ => _serviceProvider.GetRequiredService<IPaymentGatewayService>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Normalize(string? value)
|
||||||
|
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant();
|
||||||
|
}
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Services.Payment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// درگاه پرداخت بیستکارت (20Card IPG Merchant API).
|
||||||
|
/// مبلغ ورودی به تومان است. درخواستها بهصورت multipart/form-data ارسال میشوند.
|
||||||
|
/// </summary>
|
||||||
|
public class TwentyCardPaymentService : IPaymentGatewayService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<TwentyCardPaymentService> _logger;
|
||||||
|
private readonly string _terminalId;
|
||||||
|
private readonly string _terminalUsername;
|
||||||
|
private readonly string _terminalPassword;
|
||||||
|
private readonly string _defaultEmail;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
|
};
|
||||||
|
|
||||||
|
public TwentyCardPaymentService(
|
||||||
|
HttpClient httpClient,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<TwentyCardPaymentService> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
var baseUrl = configuration["TwentyCard:BaseUrl"]
|
||||||
|
?? throw new InvalidOperationException("TwentyCard:BaseUrl is not configured.");
|
||||||
|
_terminalId = configuration["TwentyCard:TerminalId"]
|
||||||
|
?? throw new InvalidOperationException("TwentyCard:TerminalId is not configured.");
|
||||||
|
_terminalUsername = configuration["TwentyCard:TerminalUsername"]
|
||||||
|
?? throw new InvalidOperationException("TwentyCard:TerminalUsername is not configured.");
|
||||||
|
_terminalPassword = configuration["TwentyCard:TerminalPassword"]
|
||||||
|
?? throw new InvalidOperationException("TwentyCard:TerminalPassword is not configured.");
|
||||||
|
_defaultEmail = configuration["TwentyCard:DefaultEmail"] ?? "info@kbs2.ir";
|
||||||
|
|
||||||
|
_httpClient.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
|
||||||
|
if (!_httpClient.DefaultRequestHeaders.Contains("Accept"))
|
||||||
|
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/json");
|
||||||
|
_logger.LogInformation("TwentyCard payment service initialized. BaseUrl={BaseUrl}", baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PaymentInitiateResult> InitiatePaymentAsync(
|
||||||
|
PaymentRequest request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// ۲۰کارت amount را به ریال میگیرد (تومان × ۱۰)
|
||||||
|
var amountRial = ((long)request.Amount) * 10L;
|
||||||
|
var amount = amountRial.ToString();
|
||||||
|
var payerName = string.IsNullOrWhiteSpace(request.Name)
|
||||||
|
? (string.IsNullOrWhiteSpace(request.Mobile) ? "کاربر" : request.Mobile)
|
||||||
|
: request.Name.Trim();
|
||||||
|
var invoice = string.IsNullOrWhiteSpace(request.Invoice)
|
||||||
|
? request.UserId.ToString()
|
||||||
|
: request.Invoice;
|
||||||
|
|
||||||
|
using var content = BuildForm(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["terminal_id"] = _terminalId,
|
||||||
|
["terminal_username"] = _terminalUsername,
|
||||||
|
["terminal_password"] = _terminalPassword,
|
||||||
|
["amount"] = amount,
|
||||||
|
["name"] = payerName,
|
||||||
|
["mobile"] = request.Mobile ?? string.Empty,
|
||||||
|
["invoice"] = invoice,
|
||||||
|
["callback"] = request.CallbackUrl,
|
||||||
|
["description"] = request.Description ?? string.Empty,
|
||||||
|
// بدون email درگاه ۲۰کارت بهجای validation، HTTP 500 برمیگرداند
|
||||||
|
["email"] = ResolveEmail(request.Email)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.NationalId))
|
||||||
|
AddFormField(content, "national_id", request.NationalId);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"TwentyCard payment request: Amount={AmountToman} Toman ({AmountRial} Rial), User={UserId}, Invoice={Invoice}",
|
||||||
|
(long)request.Amount, amountRial, request.UserId, invoice);
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("api/pgw/merchant/request", content, cancellationToken);
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"TwentyCard HTTP error on request: StatusCode={StatusCode}, Body={Body}",
|
||||||
|
(int)response.StatusCode, responseBody);
|
||||||
|
|
||||||
|
return new PaymentInitiateResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
ErrorMessage = $"خطای ارتباط با درگاه ۲۰کارت (HTTP {(int)response.StatusCode})"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<TwentyCardRequestResponse>(responseBody, JsonOptions);
|
||||||
|
var authority = result?.Data?.Authority;
|
||||||
|
var redirectUrl = result?.Data?.RedirectUrl;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(authority) || string.IsNullOrWhiteSpace(redirectUrl))
|
||||||
|
{
|
||||||
|
_logger.LogError("TwentyCard request failed. Body={Body}", responseBody);
|
||||||
|
return new PaymentInitiateResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
ErrorMessage = result?.Message
|
||||||
|
?? result?.Error?.Message
|
||||||
|
?? "ایجاد تراکنش ۲۰کارت ناموفق بود"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PaymentInitiateResult
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
RefId = authority,
|
||||||
|
GatewayUrl = redirectUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "TwentyCard InitiatePayment failed");
|
||||||
|
return new PaymentInitiateResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
ErrorMessage = "خطا در ارتباط با درگاه ۲۰کارت"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PaymentVerificationResult> VerifyPaymentAsync(
|
||||||
|
string refId,
|
||||||
|
string verificationToken,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
=> throw new NotImplementedException("TwentyCard requires VerifyPaymentAsync with amount.");
|
||||||
|
|
||||||
|
public async Task<PaymentVerificationResult> VerifyPaymentAsync(
|
||||||
|
string refId,
|
||||||
|
string verificationToken,
|
||||||
|
decimal amountInToman,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!string.Equals(verificationToken, "OK", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new PaymentVerificationResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
RefId = refId,
|
||||||
|
Message = "پرداخت توسط کاربر لغو شد یا ناموفق بود",
|
||||||
|
VerificationCode = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
using var content = BuildForm(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["terminal_id"] = _terminalId,
|
||||||
|
["terminal_username"] = _terminalUsername,
|
||||||
|
["terminal_password"] = _terminalPassword,
|
||||||
|
["authority"] = refId,
|
||||||
|
["amount"] = (((long)amountInToman) * 10L).ToString()
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("api/pgw/merchant/verify", content, cancellationToken);
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"TwentyCard verify response: Authority={Authority}, StatusCode={StatusCode}, Body={Body}",
|
||||||
|
refId, (int)response.StatusCode, responseBody);
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<TwentyCardVerifyResponse>(responseBody, JsonOptions);
|
||||||
|
|
||||||
|
var paid = result?.Status?.Paid == true;
|
||||||
|
var verified = result?.Status?.Verified == true;
|
||||||
|
var noError = result?.Error != true;
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode && noError && paid && verified)
|
||||||
|
{
|
||||||
|
return new PaymentVerificationResult
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
RefId = refId,
|
||||||
|
TrackingCode = refId,
|
||||||
|
Message = result?.Message ?? "تراکنش با موفقیت تایید شد",
|
||||||
|
VerificationCode = result?.Code ?? 200
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code 103 = already verified — treat as success for idempotency
|
||||||
|
if (result?.Code == 103)
|
||||||
|
{
|
||||||
|
return new PaymentVerificationResult
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
RefId = refId,
|
||||||
|
TrackingCode = refId,
|
||||||
|
Message = result.Message ?? "تراکنش قبلاً تایید شده است",
|
||||||
|
VerificationCode = 103
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PaymentVerificationResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
RefId = refId,
|
||||||
|
Message = result?.Message ?? "تایید تراکنش ۲۰کارت ناموفق بود",
|
||||||
|
VerificationCode = result?.Code ?? (int)response.StatusCode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "TwentyCard VerifyPayment failed. Authority={Authority}", refId);
|
||||||
|
return new PaymentVerificationResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
RefId = refId,
|
||||||
|
Message = "خطا در تایید تراکنش ۲۰کارت"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PayoutResult> ProcessPayoutAsync(
|
||||||
|
PayoutRequest request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new PayoutResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
Message = "واریز از طریق درگاه ۲۰کارت پشتیبانی نمیشود"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MultipartFormDataContent BuildForm(Dictionary<string, string> fields)
|
||||||
|
{
|
||||||
|
var form = new MultipartFormDataContent();
|
||||||
|
foreach (var (key, value) in fields)
|
||||||
|
AddFormField(form, key, value ?? string.Empty);
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شبیه curl --form: بدون Content-Type روی هر part ( بعضی بکاندهای PHP با charset=utf-8 میشکنند).
|
||||||
|
/// </summary>
|
||||||
|
private static void AddFormField(MultipartFormDataContent form, string name, string value)
|
||||||
|
{
|
||||||
|
var part = new StringContent(value ?? string.Empty);
|
||||||
|
part.Headers.ContentType = null;
|
||||||
|
form.Add(part, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveEmail(string? email)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(email) && email.Contains('@', StringComparison.Ordinal))
|
||||||
|
return email.Trim();
|
||||||
|
return _defaultEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TwentyCardRequestResponse
|
||||||
|
{
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public TwentyCardRequestData? Data { get; set; }
|
||||||
|
public TwentyCardErrorBody? Error { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TwentyCardErrorBody
|
||||||
|
{
|
||||||
|
public string? Code { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TwentyCardRequestData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("authority")]
|
||||||
|
public string? Authority { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("redirectUrl")]
|
||||||
|
public string? RedirectUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TwentyCardVerifyResponse
|
||||||
|
{
|
||||||
|
public bool Error { get; set; }
|
||||||
|
public int Code { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public TwentyCardVerifyStatus? Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TwentyCardVerifyStatus
|
||||||
|
{
|
||||||
|
public bool Paid { get; set; }
|
||||||
|
public bool Verified { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>0.0.205</Version>
|
<Version>0.0.214</Version>
|
||||||
<DebugType>None</DebugType>
|
<DebugType>None</DebugType>
|
||||||
<DebugSymbols>False</DebugSymbols>
|
<DebugSymbols>False</DebugSymbols>
|
||||||
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ enum DeliveryStatus
|
|||||||
DELIVERY_SHIPPED = 2;
|
DELIVERY_SHIPPED = 2;
|
||||||
DELIVERY_DELIVERED = 3;
|
DELIVERY_DELIVERED = 3;
|
||||||
DELIVERY_CANCELLED = 4;
|
DELIVERY_CANCELLED = 4;
|
||||||
|
DELIVERY_READY_FOR_OFFICE = 5;
|
||||||
|
DELIVERY_RETURNED = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Order By Id
|
// Get Order By Id
|
||||||
|
|||||||
@@ -209,6 +209,11 @@ message NetworkTreeNodeModel
|
|||||||
google.protobuf.BoolValue is_new_activation = 22; // true=اولین فعالسازی، false=خرید مجدد، null=بدون Cycle
|
google.protobuf.BoolValue is_new_activation = 22; // true=اولین فعالسازی، false=خرید مجدد، null=بدون Cycle
|
||||||
string package_name = 23; // نام پکیج موثر
|
string package_name = 23; // نام پکیج موثر
|
||||||
google.protobuf.Int64Value package_id = 24; // شناسه پکیج
|
google.protobuf.Int64Value package_id = 24; // شناسه پکیج
|
||||||
|
// تعداد اعضای فعال با پکیج جاری در پای چپ/راست (طلایی و نقرهای)
|
||||||
|
int32 gold_left_leg_total = 25;
|
||||||
|
int32 gold_right_leg_total = 26;
|
||||||
|
int32 silver_left_leg_total = 27;
|
||||||
|
int32 silver_right_leg_total = 28;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHistory Query
|
// GetHistory Query
|
||||||
@@ -310,6 +315,7 @@ message GetMyNetworkTreeResponse
|
|||||||
// GetMyNetworkStatistics - for frontend customer display
|
// GetMyNetworkStatistics - for frontend customer display
|
||||||
message GetMyNetworkStatisticsResponse
|
message GetMyNetworkStatisticsResponse
|
||||||
{
|
{
|
||||||
|
// اعضای فعال باشگاه: کل = چپ + راست
|
||||||
int32 total_members = 1;
|
int32 total_members = 1;
|
||||||
int32 active_members = 2;
|
int32 active_members = 2;
|
||||||
int32 left_leg_count = 3;
|
int32 left_leg_count = 3;
|
||||||
@@ -323,6 +329,11 @@ message GetMyNetworkStatisticsResponse
|
|||||||
string my_network_leg = 11;
|
string my_network_leg = 11;
|
||||||
string my_referral_code = 12;
|
string my_referral_code = 12;
|
||||||
CustomerLastMemberModel last_member = 13;
|
CustomerLastMemberModel last_member = 13;
|
||||||
|
// تعداد اعضای فعال با پکیج جاری در پای چپ/راست — نوار آمار پایین شجره (D3)
|
||||||
|
int32 gold_left_leg_total = 14;
|
||||||
|
int32 gold_right_leg_total = 15;
|
||||||
|
int32 silver_left_leg_total = 16;
|
||||||
|
int32 silver_right_leg_total = 17;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CustomerLastMemberModel
|
message CustomerLastMemberModel
|
||||||
|
|||||||
@@ -249,6 +249,8 @@ message CustomerPurchasePackageRequest
|
|||||||
int64 package_id = 1;
|
int64 package_id = 1;
|
||||||
PurchaseMethodEnum purchase_method = 2;
|
PurchaseMethodEnum purchase_method = 2;
|
||||||
string callback_url = 3;
|
string callback_url = 3;
|
||||||
|
// "zarinpal" (default) | "20card"
|
||||||
|
string gateway_provider = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CustomerPurchasePackageResponse
|
message CustomerPurchasePackageResponse
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ message PaymentTransactionModel
|
|||||||
google.protobuf.Int64Value transaction_id = 13;
|
google.protobuf.Int64Value transaction_id = 13;
|
||||||
string description = 14;
|
string description = 14;
|
||||||
google.protobuf.Timestamp created_at = 15;
|
google.protobuf.Timestamp created_at = 15;
|
||||||
|
string user_full_name = 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetPaymentTransactionSummaryRequest
|
message GetPaymentTransactionSummaryRequest
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ enum DeliveryStatus
|
|||||||
DeliveryStatus_Delivered = 3;
|
DeliveryStatus_Delivered = 3;
|
||||||
// مرجوع شده
|
// مرجوع شده
|
||||||
DeliveryStatus_Returned = 4;
|
DeliveryStatus_Returned = 4;
|
||||||
|
// لغو شده
|
||||||
|
DeliveryStatus_Cancelled = 5;
|
||||||
|
// آماده تحویل حضوری در دفتر
|
||||||
|
DeliveryStatus_ReadyForOfficePickup = 6;
|
||||||
}
|
}
|
||||||
enum TransactionType
|
enum TransactionType
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -192,6 +192,9 @@ message GetAllTransactionsByFilterResponseModel
|
|||||||
messages.TransactionType type = 7;
|
messages.TransactionType type = 7;
|
||||||
}
|
}
|
||||||
google.protobuf.Timestamp created = 8;
|
google.protobuf.Timestamp created = 8;
|
||||||
|
google.protobuf.Int64Value user_id = 9;
|
||||||
|
string user_full_name = 10;
|
||||||
|
google.protobuf.StringValue mobile = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetTransactionSummaryRequest
|
message GetTransactionSummaryRequest
|
||||||
|
|||||||
@@ -115,6 +115,16 @@ service UserContract
|
|||||||
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
rpc GetMyInvitees(GetMyInviteesRequest) returns (GetMyInviteesResponse){
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/Customer/GetMyInvitees"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
rpc GetInvitationsByFilter(GetInvitationsByFilterRequest) returns (GetInvitationsByFilterResponse){
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/GetInvitationsByFilter"
|
||||||
|
};
|
||||||
|
};
|
||||||
rpc UploadCustomerAvatar(UploadCustomerAvatarRequest) returns (UploadCustomerAvatarResponse){
|
rpc UploadCustomerAvatar(UploadCustomerAvatarRequest) returns (UploadCustomerAvatarResponse){
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
post: "/Customer/UploadAvatar"
|
post: "/Customer/UploadAvatar"
|
||||||
@@ -238,6 +248,8 @@ message GetAllUserByFilterResponseModel
|
|||||||
bool sms_notifications = 12;
|
bool sms_notifications = 12;
|
||||||
bool push_notifications = 13;
|
bool push_notifications = 13;
|
||||||
google.protobuf.Timestamp birth_date = 14;
|
google.protobuf.Timestamp birth_date = 14;
|
||||||
|
string default_address = 15;
|
||||||
|
string default_postal_code = 16;
|
||||||
}
|
}
|
||||||
message GetJwtTokenRequest
|
message GetJwtTokenRequest
|
||||||
{
|
{
|
||||||
@@ -292,6 +304,8 @@ message VerifyOtpTokenRequest
|
|||||||
string purpose = 2;
|
string purpose = 2;
|
||||||
string code = 3;
|
string code = 3;
|
||||||
google.protobuf.StringValue parent_referral_code = 4;
|
google.protobuf.StringValue parent_referral_code = 4;
|
||||||
|
// شماره موبایل دعوتکننده (اختیاری؛ فقط برای ثبتنام جدید)
|
||||||
|
google.protobuf.StringValue inviter_mobile = 5;
|
||||||
}
|
}
|
||||||
message VerifyOtpTokenResponse
|
message VerifyOtpTokenResponse
|
||||||
{
|
{
|
||||||
@@ -464,3 +478,78 @@ message UpdateCustomerSettingsRequest
|
|||||||
google.protobuf.StringValue time_zone = 6;
|
google.protobuf.StringValue time_zone = 6;
|
||||||
google.protobuf.BoolValue two_factor_auth_enabled = 7;
|
google.protobuf.BoolValue two_factor_auth_enabled = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============= Inviter (دعوتکننده) — جدا از معرف شبکه =============
|
||||||
|
|
||||||
|
message GetMyInviteesRequest
|
||||||
|
{
|
||||||
|
city.PaginationState pagination_state = 1;
|
||||||
|
// ALL | ACTIVE | INACTIVE — بر اساس ClubMembership.IsActive
|
||||||
|
google.protobuf.StringValue club_status_filter = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetMyInviteesResponse
|
||||||
|
{
|
||||||
|
city.MetaData meta_data = 1;
|
||||||
|
repeated InviteeModel invitees = 2;
|
||||||
|
InviteeStats stats = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InviteeModel
|
||||||
|
{
|
||||||
|
int64 user_id = 1;
|
||||||
|
google.protobuf.StringValue first_name = 2;
|
||||||
|
google.protobuf.StringValue last_name = 3;
|
||||||
|
string mobile = 4;
|
||||||
|
google.protobuf.Timestamp registered_at = 5;
|
||||||
|
bool is_club_active = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InviteeStats
|
||||||
|
{
|
||||||
|
int32 total_invitees = 1;
|
||||||
|
int32 club_active_count = 2;
|
||||||
|
int32 club_inactive_count = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetInvitationsByFilterRequest
|
||||||
|
{
|
||||||
|
city.PaginationState pagination_state = 1;
|
||||||
|
google.protobuf.StringValue sort_by = 2;
|
||||||
|
GetInvitationsByFilterFilter filter = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetInvitationsByFilterFilter
|
||||||
|
{
|
||||||
|
google.protobuf.StringValue inviter_mobile = 1;
|
||||||
|
google.protobuf.StringValue inviter_first_name = 2;
|
||||||
|
google.protobuf.StringValue inviter_last_name = 3;
|
||||||
|
google.protobuf.StringValue invitee_mobile = 4;
|
||||||
|
google.protobuf.StringValue invitee_first_name = 5;
|
||||||
|
google.protobuf.StringValue invitee_last_name = 6;
|
||||||
|
google.protobuf.BoolValue invitee_club_active = 7;
|
||||||
|
google.protobuf.Timestamp created_from = 8;
|
||||||
|
google.protobuf.Timestamp created_to = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetInvitationsByFilterResponse
|
||||||
|
{
|
||||||
|
city.MetaData meta_data = 1;
|
||||||
|
repeated InvitationReportModel models = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InvitationReportModel
|
||||||
|
{
|
||||||
|
int64 id = 1;
|
||||||
|
int64 inviter_user_id = 2;
|
||||||
|
google.protobuf.StringValue inviter_first_name = 3;
|
||||||
|
google.protobuf.StringValue inviter_last_name = 4;
|
||||||
|
string inviter_mobile = 5;
|
||||||
|
int64 invitee_user_id = 6;
|
||||||
|
google.protobuf.StringValue invitee_first_name = 7;
|
||||||
|
google.protobuf.StringValue invitee_last_name = 8;
|
||||||
|
string invitee_mobile = 9;
|
||||||
|
google.protobuf.Timestamp invitee_registered_at = 10;
|
||||||
|
bool invitee_club_active = 11;
|
||||||
|
google.protobuf.Timestamp invited_at = 12;
|
||||||
|
}
|
||||||
|
|||||||
@@ -220,6 +220,9 @@ message GetUserOrderResponse
|
|||||||
google.protobuf.StringValue user_national_code = 16;
|
google.protobuf.StringValue user_national_code = 16;
|
||||||
// اطلاعات مالیات بر ارزش افزوده
|
// اطلاعات مالیات بر ارزش افزوده
|
||||||
OrderVATInfo vat_info = 17;
|
OrderVATInfo vat_info = 17;
|
||||||
|
// اطلاعات پستی برای برچسب کارتن
|
||||||
|
google.protobuf.StringValue postal_code = 18;
|
||||||
|
google.protobuf.StringValue user_mobile = 19;
|
||||||
}
|
}
|
||||||
|
|
||||||
// اطلاعات مالیات بر ارزش افزوده
|
// اطلاعات مالیات بر ارزش افزوده
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ service UserPackagePurchaseContract
|
|||||||
get: "/UserPackagePurchase/GetSummary"
|
get: "/UserPackagePurchase/GetSummary"
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
rpc GetCustomerPackagePurchaseRollup(GetCustomerPackagePurchaseRollupRequest) returns (GetCustomerPackagePurchaseRollupResponse){
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/UserPackagePurchase/GetCustomerRollup"
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetAllUserPackagePurchaseByFilterRequest
|
message GetAllUserPackagePurchaseByFilterRequest
|
||||||
@@ -73,3 +78,51 @@ message GetUserPackagePurchaseSummaryResponse
|
|||||||
int64 total_count = 1;
|
int64 total_count = 1;
|
||||||
int64 total_amount = 2;
|
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,7 +95,11 @@ public class NetworkMembershipProfile : IRegister
|
|||||||
IsActivatedInTargetWeek = node.IsActivatedInTargetWeek,
|
IsActivatedInTargetWeek = node.IsActivatedInTargetWeek,
|
||||||
PackageName = node.PackageName ?? string.Empty,
|
PackageName = node.PackageName ?? string.Empty,
|
||||||
PackageId = node.PackageId.HasValue ? node.PackageId.Value : null,
|
PackageId = node.PackageId.HasValue ? node.PackageId.Value : null,
|
||||||
IsNewActivation = node.IsNewActivation.HasValue ? node.IsNewActivation.Value : null
|
IsNewActivation = node.IsNewActivation.HasValue ? node.IsNewActivation.Value : null,
|
||||||
|
GoldLeftLegTotal = node.GoldLeftLegTotal,
|
||||||
|
GoldRightLegTotal = node.GoldRightLegTotal,
|
||||||
|
SilverLeftLegTotal = node.SilverLeftLegTotal,
|
||||||
|
SilverRightLegTotal = node.SilverRightLegTotal
|
||||||
};
|
};
|
||||||
|
|
||||||
if (parentId.HasValue)
|
if (parentId.HasValue)
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ public class PaymentTransactionProfile : IRegister
|
|||||||
.Map(dest => dest.OrderId, src => src.OrderId ?? string.Empty)
|
.Map(dest => dest.OrderId, src => src.OrderId ?? string.Empty)
|
||||||
.Map(dest => dest.TransactionId, src => src.TransactionId)
|
.Map(dest => dest.TransactionId, src => src.TransactionId)
|
||||||
.Map(dest => dest.Description, src => src.Description ?? string.Empty)
|
.Map(dest => dest.Description, src => src.Description ?? string.Empty)
|
||||||
.Map(dest => dest.CreatedAt, src => Timestamp.FromDateTimeOffset(src.Created));
|
.Map(dest => dest.CreatedAt, src => Timestamp.FromDateTimeOffset(src.Created))
|
||||||
|
.Map(dest => dest.UserFullName, src => src.UserFullName ?? string.Empty);
|
||||||
|
|
||||||
config.NewConfig<ProtoPt.GetPaymentTransactionSummaryRequest, AppPtSummary.GetPaymentTransactionSummaryQuery>()
|
config.NewConfig<ProtoPt.GetPaymentTransactionSummaryRequest, AppPtSummary.GetPaymentTransactionSummaryQuery>()
|
||||||
.Map(dest => dest.Filter, src => src.Filter);
|
.Map(dest => dest.Filter, src => src.Filter);
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ public class TransactionsProfile : IRegister
|
|||||||
.Map(dest => dest.PaymentDate, src => src.PaymentDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(src.PaymentDate.Value, DateTimeKind.Utc)) : null)
|
.Map(dest => dest.PaymentDate, src => src.PaymentDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(src.PaymentDate.Value, DateTimeKind.Utc)) : null)
|
||||||
.Map(dest => dest.RefId, src => src.RefId)
|
.Map(dest => dest.RefId, src => src.RefId)
|
||||||
.Map(dest => dest.Type, src => src.Type)
|
.Map(dest => dest.Type, src => src.Type)
|
||||||
.Map(dest => dest.Created, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.Created, DateTimeKind.Utc)));
|
.Map(dest => dest.Created, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.Created, DateTimeKind.Utc)))
|
||||||
|
.Map(dest => dest.UserId, src => src.UserId)
|
||||||
|
.Map(dest => dest.UserFullName, src => src.UserFullName ?? string.Empty)
|
||||||
|
.Map(dest => dest.Mobile, src => src.Mobile);
|
||||||
|
|
||||||
config.NewConfig<ProtoTx.GetTransactionSummaryRequest, AppTxSummary.GetTransactionSummaryQuery>()
|
config.NewConfig<ProtoTx.GetTransactionSummaryRequest, AppTxSummary.GetTransactionSummaryQuery>()
|
||||||
.Map(dest => dest.Filter, src => src.Filter);
|
.Map(dest => dest.Filter, src => src.Filter);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using CMSMicroservice.Domain.Enums;
|
|||||||
using Mapster;
|
using Mapster;
|
||||||
using AppUp = CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetAllUserPackagePurchaseByFilter;
|
using AppUp = CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetAllUserPackagePurchaseByFilter;
|
||||||
using AppUpSummary = CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetUserPackagePurchaseSummary;
|
using AppUpSummary = CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetUserPackagePurchaseSummary;
|
||||||
|
using AppRollup = CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetCustomerPackagePurchaseRollup;
|
||||||
using ProtoUp = CMSMicroservice.Protobuf.Protos.UserPackagePurchase;
|
using ProtoUp = CMSMicroservice.Protobuf.Protos.UserPackagePurchase;
|
||||||
|
|
||||||
namespace CMSMicroservice.WebApi.Common.Mappings;
|
namespace CMSMicroservice.WebApi.Common.Mappings;
|
||||||
@@ -49,5 +50,41 @@ public class UserPackagePurchaseProfile : IRegister
|
|||||||
config.NewConfig<AppUpSummary.GetUserPackagePurchaseSummaryResponseDto, ProtoUp.GetUserPackagePurchaseSummaryResponse>()
|
config.NewConfig<AppUpSummary.GetUserPackagePurchaseSummaryResponseDto, ProtoUp.GetUserPackagePurchaseSummaryResponse>()
|
||||||
.Map(dest => dest.TotalCount, src => src.TotalCount)
|
.Map(dest => dest.TotalCount, src => src.TotalCount)
|
||||||
.Map(dest => dest.TotalAmount, src => src.TotalAmount);
|
.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,7 +4,10 @@ public class UserProfile : IRegister
|
|||||||
{
|
{
|
||||||
void IRegister.Register(TypeAdapterConfig config)
|
void IRegister.Register(TypeAdapterConfig config)
|
||||||
{
|
{
|
||||||
//config.NewConfig<Source,Destination>()
|
config.NewConfig<Application.UserCQ.Queries.GetAllUserByFilter.GetAllUserByFilterResponseModel,
|
||||||
// .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}");
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||||
|
|
||||||
FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS base
|
FROM 185.252.31.44:32082/dotnet/aspnet:9.0 AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
EXPOSE 443
|
EXPOSE 443
|
||||||
|
|
||||||
FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build
|
FROM 185.252.31.44:32082/dotnet/sdk:9.0 AS build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj", "CMSMicroservice.WebApi/"]
|
COPY ["CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj", "CMSMicroservice.WebApi/"]
|
||||||
COPY ["CMSMicroservice.Application/CMSMicroservice.Application.csproj", "CMSMicroservice.Application/"]
|
COPY ["CMSMicroservice.Application/CMSMicroservice.Application.csproj", "CMSMicroservice.Application/"]
|
||||||
|
|||||||
@@ -99,7 +99,20 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB
|
|||||||
|
|
||||||
public override async Task<UpdateOrderStatusResponse> UpdateOrderStatus(UpdateOrderStatusRequest request, ServerCallContext context)
|
public override async Task<UpdateOrderStatusResponse> UpdateOrderStatus(UpdateOrderStatusRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return await _dispatchRequestToCQRS.Handle<UpdateOrderStatusRequest, UpdateOrderStatusCommand, UpdateOrderStatusResponse>(request, 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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<GetOrderByIdResponse> GetOrderById(GetOrderByIdRequest request, ServerCallContext context)
|
public override async Task<GetOrderByIdResponse> GetOrderById(GetOrderByIdRequest request, ServerCallContext context)
|
||||||
@@ -141,6 +154,8 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB
|
|||||||
Address = result.Address.Address ?? "",
|
Address = result.Address.Address ?? "",
|
||||||
PostalCode = result.Address.PostalCode ?? ""
|
PostalCode = result.Address.PostalCode ?? ""
|
||||||
};
|
};
|
||||||
|
if (!string.IsNullOrWhiteSpace(result.Address.Phone))
|
||||||
|
response.Address.Phone = result.Address.Phone;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var item in result.Items)
|
foreach (var item in result.Items)
|
||||||
@@ -197,9 +212,98 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<GetAllDiscountOrdersResponse> GetAllDiscountOrders(GetAllDiscountOrdersRequest request, ServerCallContext context)
|
public override async Task<GetAllDiscountOrdersResponse> GetAllDiscountOrders(
|
||||||
|
GetAllDiscountOrdersRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return await _dispatchRequestToCQRS.Handle<GetAllDiscountOrdersRequest, GetAllDiscountOrdersQuery, GetAllDiscountOrdersResponse>(request, 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<GetDiscountSalesReportResponse> GetDiscountSalesReport(GetDiscountSalesReportRequest request, ServerCallContext context)
|
public override async Task<GetDiscountSalesReportResponse> GetDiscountSalesReport(GetDiscountSalesReportRequest request, ServerCallContext context)
|
||||||
@@ -359,14 +463,50 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB
|
|||||||
_ => CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus.PaymentPending
|
_ => 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
|
private static DeliveryStatus MapDeliveryStatus(DomainEnums.DeliveryStatus status) => status switch
|
||||||
{
|
{
|
||||||
DomainEnums.DeliveryStatus.None => DeliveryStatus.DeliveryPending,
|
DomainEnums.DeliveryStatus.None => DeliveryStatus.DeliveryPending,
|
||||||
DomainEnums.DeliveryStatus.Pending => DeliveryStatus.DeliveryProcessing,
|
DomainEnums.DeliveryStatus.Pending => DeliveryStatus.DeliveryProcessing,
|
||||||
DomainEnums.DeliveryStatus.InTransit => DeliveryStatus.DeliveryShipped,
|
DomainEnums.DeliveryStatus.InTransit => DeliveryStatus.DeliveryShipped,
|
||||||
DomainEnums.DeliveryStatus.Delivered => DeliveryStatus.DeliveryDelivered,
|
DomainEnums.DeliveryStatus.Delivered => DeliveryStatus.DeliveryDelivered,
|
||||||
DomainEnums.DeliveryStatus.Returned => DeliveryStatus.DeliveryCancelled,
|
|
||||||
DomainEnums.DeliveryStatus.Cancelled => DeliveryStatus.DeliveryCancelled,
|
DomainEnums.DeliveryStatus.Cancelled => DeliveryStatus.DeliveryCancelled,
|
||||||
|
DomainEnums.DeliveryStatus.ReadyForOfficePickup => DeliveryStatus.DeliveryReadyForOffice,
|
||||||
|
DomainEnums.DeliveryStatus.Returned => DeliveryStatus.DeliveryReturned,
|
||||||
_ => DeliveryStatus.DeliveryPending
|
_ => 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,6 +185,11 @@ public class NetworkMembershipService : NetworkMembershipContract.NetworkMembers
|
|||||||
node.PackageId = dto.PackageId.Value;
|
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)
|
if (dto.IsNewActivation.HasValue)
|
||||||
{
|
{
|
||||||
node.IsNewActivation = dto.IsNewActivation.Value;
|
node.IsNewActivation = dto.IsNewActivation.Value;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
private readonly ISender _sender;
|
private readonly ISender _sender;
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly ICurrentUserService _currentUserService;
|
private readonly ICurrentUserService _currentUserService;
|
||||||
private readonly IPaymentGatewayService _paymentGateway;
|
private readonly IPaymentGatewayResolver _paymentGatewayResolver;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly IUserPaymentLock _paymentLock;
|
private readonly IUserPaymentLock _paymentLock;
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
ISender sender,
|
ISender sender,
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
ICurrentUserService currentUserService,
|
ICurrentUserService currentUserService,
|
||||||
IPaymentGatewayService paymentGateway,
|
IPaymentGatewayResolver paymentGatewayResolver,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IUserPaymentLock paymentLock)
|
IUserPaymentLock paymentLock)
|
||||||
{
|
{
|
||||||
@@ -49,7 +49,7 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
_sender = sender;
|
_sender = sender;
|
||||||
_context = context;
|
_context = context;
|
||||||
_currentUserService = currentUserService;
|
_currentUserService = currentUserService;
|
||||||
_paymentGateway = paymentGateway;
|
_paymentGatewayResolver = paymentGatewayResolver;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_paymentLock = paymentLock;
|
_paymentLock = paymentLock;
|
||||||
}
|
}
|
||||||
@@ -172,18 +172,37 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
if (package == null)
|
if (package == null)
|
||||||
throw new RpcException(new Status(StatusCode.NotFound, "پکیج مورد نظر یافت نشد"));
|
throw new RpcException(new Status(StatusCode.NotFound, "پکیج مورد نظر یافت نشد"));
|
||||||
|
|
||||||
// Create transaction
|
var gatewayProvider = NormalizeGatewayProvider(request.GatewayProvider);
|
||||||
|
var isTwentyCard = gatewayProvider == "20card";
|
||||||
|
|
||||||
|
long gatewayAmount = package.Price;
|
||||||
|
if (isTwentyCard)
|
||||||
|
{
|
||||||
|
var packageKey = $"TwentyCard:Packages:{package.Id}";
|
||||||
|
gatewayAmount = _configuration.GetValue<long?>($"{packageKey}:GatewayAmountToman") ?? 0;
|
||||||
|
var walletCredit = _configuration.GetValue<long?>($"{packageKey}:WalletCreditToman") ?? 0;
|
||||||
|
if (gatewayAmount <= 0 || walletCredit <= 0)
|
||||||
|
{
|
||||||
|
throw new RpcException(new Status(
|
||||||
|
StatusCode.FailedPrecondition,
|
||||||
|
"پرداخت ۲۰کارت برای این پکیج پیکربندی نشده است"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create transaction — 20card: Amount = gateway charge; zarinpal: Package.Price
|
||||||
var transaction = new CMSMicroservice.Domain.Entities.Transaction
|
var transaction = new CMSMicroservice.Domain.Entities.Transaction
|
||||||
{
|
{
|
||||||
Amount = package.Price,
|
Amount = gatewayAmount,
|
||||||
Description = $"خرید پکیج {package.Title}",
|
Description = isTwentyCard
|
||||||
|
? $"خرید پکیج {package.Title} (۲۰کارت)"
|
||||||
|
: $"خرید پکیج {package.Title}",
|
||||||
PaymentStatus = Domain.Enums.PaymentStatus.Pending,
|
PaymentStatus = Domain.Enums.PaymentStatus.Pending,
|
||||||
Type = Domain.Enums.TransactionType.Buy
|
Type = Domain.Enums.TransactionType.Buy
|
||||||
};
|
};
|
||||||
_context.Transactions.Add(transaction);
|
_context.Transactions.Add(transaction);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// Create purchase record
|
// Create purchase record — UPP.Amount always Package.Price (catalog)
|
||||||
var purchaseMethod = request.PurchaseMethod == PurchaseMethodEnum.PurchaseMethodGateway
|
var purchaseMethod = request.PurchaseMethod == PurchaseMethodEnum.PurchaseMethodGateway
|
||||||
? Domain.Enums.PackagePurchaseMethod.DirectPurchase
|
? Domain.Enums.PackagePurchaseMethod.DirectPurchase
|
||||||
: Domain.Enums.PackagePurchaseMethod.DayaLoan;
|
: Domain.Enums.PackagePurchaseMethod.DayaLoan;
|
||||||
@@ -204,20 +223,32 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
var user = await _context.Users
|
var user = await _context.Users
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(u => u.Id == userId)
|
.Where(u => u.Id == userId)
|
||||||
.Select(u => new { u.Mobile })
|
.Select(u => new { u.Mobile, u.FirstName, u.LastName, u.Email, u.NationalCode })
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
// Callback URL از config — نه از ورودی کاربر (امنیت)
|
// Callback URL از config — نه از ورودی کاربر (امنیت)
|
||||||
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
|
// ۲۰کارت: CallbackBaseUrl اختیاری (مثلاً استیج → kbs2.ir چون دامنه استیج در ترمینال ثبت نیست)
|
||||||
var callbackUrl = $"{frontOfficeBaseUrl}/profile/payment-callback?orderId={purchase.Id}";
|
var frontOfficeBaseUrl = isTwentyCard
|
||||||
|
? (_configuration["TwentyCard:CallbackBaseUrl"]
|
||||||
|
?? _configuration["FrontOfficeBaseUrl"]
|
||||||
|
?? "https://localhost:5268")
|
||||||
|
: (_configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268");
|
||||||
|
var callbackUrl = $"{frontOfficeBaseUrl.TrimEnd('/')}/profile/payment-callback?orderId={purchase.Id}";
|
||||||
|
|
||||||
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
|
var paymentGateway = _paymentGatewayResolver.Get(gatewayProvider);
|
||||||
|
var payerName = $"{user?.FirstName} {user?.LastName}".Trim();
|
||||||
|
|
||||||
|
var paymentResult = await paymentGateway.InitiatePaymentAsync(new PaymentRequest
|
||||||
{
|
{
|
||||||
Amount = package.Price,
|
Amount = gatewayAmount,
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
Mobile = user?.Mobile ?? string.Empty,
|
Mobile = user?.Mobile ?? string.Empty,
|
||||||
|
Name = payerName,
|
||||||
|
Invoice = purchase.Id.ToString(),
|
||||||
Description = $"خرید پکیج {package.Title}",
|
Description = $"خرید پکیج {package.Title}",
|
||||||
CallbackUrl = callbackUrl
|
CallbackUrl = callbackUrl,
|
||||||
|
Email = user?.Email,
|
||||||
|
NationalId = user?.NationalCode
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
if (!paymentResult.IsSuccess)
|
if (!paymentResult.IsSuccess)
|
||||||
@@ -235,12 +266,14 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
// Save RefId
|
// Save RefId
|
||||||
transaction.RefId = paymentResult.RefId;
|
transaction.RefId = paymentResult.RefId;
|
||||||
|
|
||||||
// ثبت PaymentTransaction
|
// ثبت PaymentTransaction — Amount = gateway amount
|
||||||
var paymentTx = new PaymentTransaction
|
var paymentTx = new PaymentTransaction
|
||||||
{
|
{
|
||||||
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
|
GatewayProvider = gatewayProvider,
|
||||||
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
|
MerchantId = isTwentyCard
|
||||||
Amount = package.Price,
|
? (_configuration["TwentyCard:TerminalId"] ?? "")
|
||||||
|
: (_configuration["ZarinPal:MerchantId"] ?? ""),
|
||||||
|
Amount = gatewayAmount,
|
||||||
CallbackUrl = callbackUrl,
|
CallbackUrl = callbackUrl,
|
||||||
Description = $"خرید پکیج {package.Title}",
|
Description = $"خرید پکیج {package.Title}",
|
||||||
Mobile = user?.Mobile,
|
Mobile = user?.Mobile,
|
||||||
@@ -265,6 +298,14 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizeGatewayProvider(string? provider)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(provider))
|
||||||
|
return "zarinpal";
|
||||||
|
var key = provider.Trim().ToLowerInvariant();
|
||||||
|
return key is "20card" or "twentycard" ? "20card" : "zarinpal";
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task<CustomerVerifyPackagePurchaseResponse> CustomerVerifyPackagePurchase(
|
public override async Task<CustomerVerifyPackagePurchaseResponse> CustomerVerifyPackagePurchase(
|
||||||
CustomerVerifyPackagePurchaseRequest request,
|
CustomerVerifyPackagePurchaseRequest request,
|
||||||
ServerCallContext context)
|
ServerCallContext context)
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
|
|||||||
TrackingCode = result.TrackingCode,
|
TrackingCode = result.TrackingCode,
|
||||||
DeliveryDescription = result.DeliveryDescription,
|
DeliveryDescription = result.DeliveryDescription,
|
||||||
UserFullName = result.UserFullName,
|
UserFullName = result.UserFullName,
|
||||||
UserNationalCode = result.UserNationalCode
|
UserNationalCode = result.UserNationalCode,
|
||||||
|
PostalCode = result.PostalCode,
|
||||||
|
UserMobile = result.UserMobile
|
||||||
};
|
};
|
||||||
|
|
||||||
// VAT Info
|
// VAT Info
|
||||||
@@ -789,7 +791,9 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
|
|||||||
TrackingCode = result.TrackingCode,
|
TrackingCode = result.TrackingCode,
|
||||||
DeliveryDescription = result.DeliveryDescription,
|
DeliveryDescription = result.DeliveryDescription,
|
||||||
UserFullName = result.UserFullName,
|
UserFullName = result.UserFullName,
|
||||||
UserNationalCode = result.UserNationalCode
|
UserNationalCode = result.UserNationalCode,
|
||||||
|
PostalCode = result.PostalCode,
|
||||||
|
UserMobile = result.UserMobile
|
||||||
};
|
};
|
||||||
|
|
||||||
// VAT Info
|
// VAT Info
|
||||||
@@ -1054,6 +1058,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
|
|||||||
Domain.Enums.DeliveryStatus.Delivered => "تحویل داده شده",
|
Domain.Enums.DeliveryStatus.Delivered => "تحویل داده شده",
|
||||||
Domain.Enums.DeliveryStatus.Returned => "مرجوع شده",
|
Domain.Enums.DeliveryStatus.Returned => "مرجوع شده",
|
||||||
Domain.Enums.DeliveryStatus.Cancelled => "لغو شده",
|
Domain.Enums.DeliveryStatus.Cancelled => "لغو شده",
|
||||||
|
Domain.Enums.DeliveryStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
|
||||||
_ => status.ToString()
|
_ => status.ToString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using CMSMicroservice.Protobuf.Protos.UserPackagePurchase;
|
|||||||
using CMSMicroservice.WebApi.Common.Services;
|
using CMSMicroservice.WebApi.Common.Services;
|
||||||
using CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetAllUserPackagePurchaseByFilter;
|
using CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetAllUserPackagePurchaseByFilter;
|
||||||
using CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetUserPackagePurchaseSummary;
|
using CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetUserPackagePurchaseSummary;
|
||||||
|
using CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetCustomerPackagePurchaseRollup;
|
||||||
|
|
||||||
namespace CMSMicroservice.WebApi.Services;
|
namespace CMSMicroservice.WebApi.Services;
|
||||||
|
|
||||||
@@ -18,4 +19,8 @@ public class UserPackagePurchaseService : UserPackagePurchaseContract.UserPackag
|
|||||||
public override Task<GetUserPackagePurchaseSummaryResponse> GetUserPackagePurchaseSummary(
|
public override Task<GetUserPackagePurchaseSummaryResponse> GetUserPackagePurchaseSummary(
|
||||||
GetUserPackagePurchaseSummaryRequest request, ServerCallContext context) =>
|
GetUserPackagePurchaseSummaryRequest request, ServerCallContext context) =>
|
||||||
_dispatch.Handle<GetUserPackagePurchaseSummaryRequest, GetUserPackagePurchaseSummaryQuery, GetUserPackagePurchaseSummaryResponse>(request, context);
|
_dispatch.Handle<GetUserPackagePurchaseSummaryRequest, GetUserPackagePurchaseSummaryQuery, GetUserPackagePurchaseSummaryResponse>(request, context);
|
||||||
|
|
||||||
|
public override Task<GetCustomerPackagePurchaseRollupResponse> GetCustomerPackagePurchaseRollup(
|
||||||
|
GetCustomerPackagePurchaseRollupRequest request, ServerCallContext context) =>
|
||||||
|
_dispatch.Handle<GetCustomerPackagePurchaseRollupRequest, GetCustomerPackagePurchaseRollupQuery, GetCustomerPackagePurchaseRollupResponse>(request, context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ using CMSMicroservice.Application.UserCQ.Commands.VerifyOtpToken;
|
|||||||
using CMSMicroservice.Application.UserCQ.Commands.AcceptContract;
|
using CMSMicroservice.Application.UserCQ.Commands.AcceptContract;
|
||||||
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerProfile;
|
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerProfile;
|
||||||
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerReferrals;
|
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerReferrals;
|
||||||
|
using CMSMicroservice.Application.UserCQ.Queries.GetMyInvitees;
|
||||||
|
using CMSMicroservice.Application.UserCQ.Queries.GetInvitationsByFilter;
|
||||||
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerSettings;
|
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerSettings;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common.Authorization;
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
@@ -312,6 +315,117 @@ public class UserService : UserContract.UserContractBase
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task<GetMyInviteesResponse> GetMyInvitees(
|
||||||
|
GetMyInviteesRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
var result = await _sender.Send(new GetMyInviteesQuery
|
||||||
|
{
|
||||||
|
UserId = 0,
|
||||||
|
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
|
||||||
|
ClubStatusFilter = request.ClubStatusFilter
|
||||||
|
}, context.CancellationToken);
|
||||||
|
|
||||||
|
var response = new GetMyInviteesResponse
|
||||||
|
{
|
||||||
|
MetaData = new MetaData
|
||||||
|
{
|
||||||
|
CurrentPage = result.MetaData.CurrentPage,
|
||||||
|
TotalPage = result.MetaData.TotalPage,
|
||||||
|
PageSize = result.MetaData.PageSize,
|
||||||
|
TotalCount = result.MetaData.TotalCount,
|
||||||
|
HasPrevious = result.MetaData.HasPrevious,
|
||||||
|
HasNext = result.MetaData.HasNext
|
||||||
|
},
|
||||||
|
Stats = new InviteeStats
|
||||||
|
{
|
||||||
|
TotalInvitees = result.Stats.TotalInvitees,
|
||||||
|
ClubActiveCount = result.Stats.ClubActiveCount,
|
||||||
|
ClubInactiveCount = result.Stats.ClubInactiveCount
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var item in result.Invitees)
|
||||||
|
{
|
||||||
|
response.Invitees.Add(new InviteeModel
|
||||||
|
{
|
||||||
|
UserId = item.UserId,
|
||||||
|
FirstName = item.FirstName,
|
||||||
|
LastName = item.LastName,
|
||||||
|
Mobile = item.Mobile,
|
||||||
|
RegisteredAt = Timestamp.FromDateTime(DateTime.SpecifyKind(item.RegisteredAt, DateTimeKind.Utc)),
|
||||||
|
IsClubActive = item.IsClubActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RequiresPermission(PermissionNames.UsersView)]
|
||||||
|
public override async Task<GetInvitationsByFilterResponse> GetInvitationsByFilter(
|
||||||
|
GetInvitationsByFilterRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
DateTime? createdFrom = null;
|
||||||
|
DateTime? createdTo = null;
|
||||||
|
if (request.Filter?.CreatedFrom != null)
|
||||||
|
createdFrom = request.Filter.CreatedFrom.ToDateTime();
|
||||||
|
if (request.Filter?.CreatedTo != null)
|
||||||
|
createdTo = request.Filter.CreatedTo.ToDateTime();
|
||||||
|
|
||||||
|
var result = await _sender.Send(new GetInvitationsByFilterQuery
|
||||||
|
{
|
||||||
|
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
|
||||||
|
SortBy = request.SortBy,
|
||||||
|
Filter = request.Filter == null ? null : new GetInvitationsByFilterFilterDto
|
||||||
|
{
|
||||||
|
InviterMobile = request.Filter.InviterMobile,
|
||||||
|
InviterFirstName = request.Filter.InviterFirstName,
|
||||||
|
InviterLastName = request.Filter.InviterLastName,
|
||||||
|
InviteeMobile = request.Filter.InviteeMobile,
|
||||||
|
InviteeFirstName = request.Filter.InviteeFirstName,
|
||||||
|
InviteeLastName = request.Filter.InviteeLastName,
|
||||||
|
InviteeClubActive = request.Filter.InviteeClubActive,
|
||||||
|
CreatedFrom = createdFrom,
|
||||||
|
CreatedTo = createdTo
|
||||||
|
}
|
||||||
|
}, context.CancellationToken);
|
||||||
|
|
||||||
|
var response = new GetInvitationsByFilterResponse
|
||||||
|
{
|
||||||
|
MetaData = new MetaData
|
||||||
|
{
|
||||||
|
CurrentPage = result.MetaData.CurrentPage,
|
||||||
|
TotalPage = result.MetaData.TotalPage,
|
||||||
|
PageSize = result.MetaData.PageSize,
|
||||||
|
TotalCount = result.MetaData.TotalCount,
|
||||||
|
HasPrevious = result.MetaData.HasPrevious,
|
||||||
|
HasNext = result.MetaData.HasNext
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var m in result.Models)
|
||||||
|
{
|
||||||
|
response.Models.Add(new InvitationReportModel
|
||||||
|
{
|
||||||
|
Id = m.Id,
|
||||||
|
InviterUserId = m.InviterUserId,
|
||||||
|
InviterFirstName = m.InviterFirstName,
|
||||||
|
InviterLastName = m.InviterLastName,
|
||||||
|
InviterMobile = m.InviterMobile,
|
||||||
|
InviteeUserId = m.InviteeUserId,
|
||||||
|
InviteeFirstName = m.InviteeFirstName,
|
||||||
|
InviteeLastName = m.InviteeLastName,
|
||||||
|
InviteeMobile = m.InviteeMobile,
|
||||||
|
InviteeRegisteredAt = Timestamp.FromDateTime(DateTime.SpecifyKind(m.InviteeRegisteredAt, DateTimeKind.Utc)),
|
||||||
|
InviteeClubActive = m.InviteeClubActive,
|
||||||
|
InvitedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(m.InvitedAt, DateTimeKind.Utc))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task<UploadCustomerAvatarResponse> UploadCustomerAvatar(UploadCustomerAvatarRequest request, ServerCallContext context)
|
public override async Task<UploadCustomerAvatarResponse> UploadCustomerAvatar(UploadCustomerAvatarRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
if (request.FileData == null || request.FileData.Length == 0)
|
if (request.FileData == null || request.FileData.Length == 0)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory;
|
|||||||
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawals;
|
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawals;
|
||||||
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawalSettings;
|
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawalSettings;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common;
|
||||||
using CMSMicroservice.Application.Common.Exceptions;
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
using CMSMicroservice.Domain.Common;
|
using CMSMicroservice.Domain.Common;
|
||||||
using CMSMicroservice.Domain.Enums;
|
using CMSMicroservice.Domain.Enums;
|
||||||
@@ -175,7 +176,18 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
|
|||||||
|
|
||||||
// Update payout with withdrawal info
|
// Update payout with withdrawal info
|
||||||
payout.WithdrawalMethod = (Domain.Enums.WithdrawalMethod)request.WithdrawalMethod;
|
payout.WithdrawalMethod = (Domain.Enums.WithdrawalMethod)request.WithdrawalMethod;
|
||||||
payout.IbanNumber = request.IbanNumber;
|
if (payout.WithdrawalMethod == Domain.Enums.WithdrawalMethod.Cash)
|
||||||
|
{
|
||||||
|
var normalizedIban = IbanNormalizer.TryNormalize(request.IbanNumber);
|
||||||
|
if (normalizedIban is null)
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument,
|
||||||
|
"فرمت شماره شبا معتبر نیست. باید IR و ۲۴ رقم باشد."));
|
||||||
|
payout.IbanNumber = normalizedIban;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
payout.IbanNumber = null;
|
||||||
|
}
|
||||||
payout.Status = Domain.Enums.CommissionPayoutStatus.WithdrawRequested;
|
payout.Status = Domain.Enums.CommissionPayoutStatus.WithdrawRequested;
|
||||||
|
|
||||||
await _context.SaveChangesAsync(context.CancellationToken);
|
await _context.SaveChangesAsync(context.CancellationToken);
|
||||||
|
|||||||
@@ -4,6 +4,24 @@
|
|||||||
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
||||||
"UseSandbox": false
|
"UseSandbox": false
|
||||||
},
|
},
|
||||||
|
"TwentyCard": {
|
||||||
|
"BaseUrl": "https://gateway.20card.ir/ipg",
|
||||||
|
"TerminalId": "165674",
|
||||||
|
"TerminalUsername": "ea5y1biehi",
|
||||||
|
"TerminalPassword": "SaLamAT@1404!",
|
||||||
|
"CreditPortalUrl": "https://userpanel.20card.ir/",
|
||||||
|
"DefaultEmail": "info@kbs2.ir",
|
||||||
|
"Packages": {
|
||||||
|
"4": {
|
||||||
|
"GatewayAmountToman": 9700000,
|
||||||
|
"WalletCreditToman": 13000000
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"GatewayAmountToman": 97000000,
|
||||||
|
"WalletCreditToman": 130000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CmsBaseUrl": "https://cms.kbs2.ir",
|
"CmsBaseUrl": "https://cms.kbs2.ir",
|
||||||
"FrontOfficeBaseUrl": "https://kbs2.ir",
|
"FrontOfficeBaseUrl": "https://kbs2.ir",
|
||||||
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"PaymentProvider": "zarinpal",
|
||||||
|
"ZarinPal": {
|
||||||
|
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
||||||
|
"UseSandbox": true
|
||||||
|
},
|
||||||
|
"TwentyCard": {
|
||||||
|
"BaseUrl": "https://gateway.20card.ir/ipg",
|
||||||
|
"TerminalId": "165674",
|
||||||
|
"TerminalUsername": "ea5y1biehi",
|
||||||
|
"TerminalPassword": "SaLamAT@1404!",
|
||||||
|
"CreditPortalUrl": "https://userpanel.20card.ir/",
|
||||||
|
"CallbackBaseUrl": "https://kbs2.ir",
|
||||||
|
"DefaultEmail": "info@kbs2.ir",
|
||||||
|
"Packages": {
|
||||||
|
"4": {
|
||||||
|
"GatewayAmountToman": 11200000,
|
||||||
|
"WalletCreditToman": 12656000
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"GatewayAmountToman": 11200000,
|
||||||
|
"WalletCreditToman": 12656000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CmsBaseUrl": "https://cms.se.kbs1.ir",
|
||||||
|
"FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir",
|
||||||
|
"FMS": {
|
||||||
|
"Address": "https://dl.afrino.co"
|
||||||
|
},
|
||||||
|
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
||||||
|
"JwtIssuer": "https://localhost",
|
||||||
|
"JwtAudience": "https://localhost",
|
||||||
|
"JwtExpiryInDays": 5,
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Data Source=185.252.31.44,31433; Initial Catalog=Foursat;User ID=sa;Password=87zH26nbqT;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
|
||||||
|
"providerName": "System.Data.SqlClient"
|
||||||
|
},
|
||||||
|
"Otp": {
|
||||||
|
"Secret": "K2w8k1h1mH2Qz1kqWk0c8kQ2Pq8q9H1eE2nqN1qQ8x7M=",
|
||||||
|
"MasterCode": "1122334455"
|
||||||
|
},
|
||||||
|
"Monitoring": {
|
||||||
|
"SentryEnabled": false,
|
||||||
|
"SentryDsn": "",
|
||||||
|
"SlackEnabled": false,
|
||||||
|
"SlackWebhookUrl": "",
|
||||||
|
"EmailAlertsEnabled": false,
|
||||||
|
"AdminEmails": [
|
||||||
|
"admin@example.com"
|
||||||
|
],
|
||||||
|
"SmsNotificationsEnabled": false,
|
||||||
|
"SmsApiKey": "",
|
||||||
|
"SmsGatewayUrl": ""
|
||||||
|
},
|
||||||
|
"Email": {
|
||||||
|
"Enabled": true,
|
||||||
|
"SmtpHost": "smtp.gmail.com",
|
||||||
|
"SmtpPort": 587,
|
||||||
|
"SmtpUsername": "your-email@gmail.com",
|
||||||
|
"SmtpPassword": "your-app-password",
|
||||||
|
"FromEmail": "noreply@foursat.com",
|
||||||
|
"FromName": "FourSat CMS",
|
||||||
|
"EnableSsl": true
|
||||||
|
},
|
||||||
|
"Sms": {
|
||||||
|
"Enabled": true,
|
||||||
|
"Provider": "Kavenegar",
|
||||||
|
"KavenegarApiKey": "43676A4E786A6C50452F2F6252507939346A5145764B33566B456454374E657A614468706658656D6534593D",
|
||||||
|
"Sender": "1000001110100"
|
||||||
|
},
|
||||||
|
"DayaPayment": {
|
||||||
|
"BaseUrl": "https://api.daya.ir",
|
||||||
|
"ApiKey": "YOUR_DAYA_API_KEY"
|
||||||
|
},
|
||||||
|
"DayaApi": {
|
||||||
|
"UseMock": false,
|
||||||
|
"BaseAddress": "https://Dayadiamond.ir",
|
||||||
|
"MerchantPermissionKey": "56146364$04sXjethI5WxhItR1Q9xnmFdJzl2BB8Bclsq8dAy7YVSZp3vtt-wP7ivrcCvmKLq",
|
||||||
|
"CacheDurationMinutes": 20
|
||||||
|
},
|
||||||
|
"Chatika": {
|
||||||
|
"Enabled": true,
|
||||||
|
"BaseUrl": "https://api.chatika.ir",
|
||||||
|
"ApiKey": "tIukvL8dnV4cB3yVWcCD9Xyfbj8rBxm5wPt2mLyJCgTsBBoMTWjt6mFEqQwpw-er"
|
||||||
|
},
|
||||||
|
"BackgroundJobs": {
|
||||||
|
"WeeklyCommissionCalculation": {
|
||||||
|
"Enabled": true,
|
||||||
|
"CronExpression": "5 0 * * 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SeedWorkers": {
|
||||||
|
"MagicWalletCycleSeed": {
|
||||||
|
"Enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Kestrel": {
|
||||||
|
"EndpointDefaults": {
|
||||||
|
"Protocols": "Http2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Authentication": {
|
||||||
|
"Authority": "https://ids.domain.com/",
|
||||||
|
"Audience": "domain_api"
|
||||||
|
},
|
||||||
|
"Seq": {
|
||||||
|
"ServerUrl": "https://seq.afrino.co",
|
||||||
|
"ApiKey": "oxpvpUzU1pZxMS4s3Fqq"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,24 @@
|
|||||||
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
||||||
"UseSandbox": false
|
"UseSandbox": false
|
||||||
},
|
},
|
||||||
|
"TwentyCard": {
|
||||||
|
"BaseUrl": "https://gateway.20card.ir/ipg",
|
||||||
|
"TerminalId": "165674",
|
||||||
|
"TerminalUsername": "ea5y1biehi",
|
||||||
|
"TerminalPassword": "SaLamAT@1404!",
|
||||||
|
"CreditPortalUrl": "https://userpanel.20card.ir/",
|
||||||
|
"DefaultEmail": "info@kbs2.ir",
|
||||||
|
"Packages": {
|
||||||
|
"4": {
|
||||||
|
"GatewayAmountToman": 11200000,
|
||||||
|
"WalletCreditToman": 12656000
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"GatewayAmountToman": 11200000,
|
||||||
|
"WalletCreditToman": 12656000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CmsBaseUrl": "https://cms.kbs2.ir",
|
"CmsBaseUrl": "https://cms.kbs2.ir",
|
||||||
"FrontOfficeBaseUrl": "https://kbs2.ir",
|
"FrontOfficeBaseUrl": "https://kbs2.ir",
|
||||||
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
||||||
|
|||||||
+2
-2
@@ -3,8 +3,8 @@
|
|||||||
<packageSources>
|
<packageSources>
|
||||||
<clear />
|
<clear />
|
||||||
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
|
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
|
||||||
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
|
<add key="Nexus" value="http://185.252.31.44:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
|
||||||
<add key="foursat-hosted" value="http://194.5.195.53:32081/repository/foursat-nuget-hosted/index.json" allowInsecureConnections="true" />
|
<add key="foursat-hosted" value="http://185.252.31.44:32081/repository/foursat-nuget-hosted/index.json" allowInsecureConnections="true" />
|
||||||
</packageSources>
|
</packageSources>
|
||||||
<packageSourceCredentials>
|
<packageSourceCredentials>
|
||||||
<Nexus>
|
<Nexus>
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
/*
|
||||||
|
One-time cleanup: ClubMemberships without a successful UserPackagePurchases row.
|
||||||
|
|
||||||
|
Goal after this script:
|
||||||
|
every non-deleted ClubMembership user has >= 1 successful UPP
|
||||||
|
(TransactionId IS NULL OR Transaction.PaymentStatus = 0 / Success)
|
||||||
|
|
||||||
|
Priority for missing rows:
|
||||||
|
1) ManualPayments Approved + successful Transaction -> reuse same TransactionId
|
||||||
|
PurchaseMethod = Manual (3)
|
||||||
|
2) else ClubMembershipCycles / membership dates -> create synthetic Success Transaction + UPP
|
||||||
|
PurchaseMethod priority:
|
||||||
|
Cycle.PurchaseMethod (if not None)
|
||||||
|
-> ClubMembership.PurchaseMethod (if not None)
|
||||||
|
-> Users.PackagePurchaseMethod (if not None)
|
||||||
|
-> Manual (3) as last resort
|
||||||
|
|
||||||
|
Safe to re-run (skips users who already have a successful UPP).
|
||||||
|
|
||||||
|
Run against CMS database once, then verify Orphans left = 0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
BEGIN TRAN;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 1) From ManualPayments (reuse existing Success transaction)
|
||||||
|
------------------------------------------------------------
|
||||||
|
;WITH OrphanClubUsers AS (
|
||||||
|
SELECT m.UserId,
|
||||||
|
COALESCE(NULLIF(m.LastPackageId, 0), NULLIF(m.FirstPackageId, 0)) AS PackageIdHint
|
||||||
|
FROM CMS.ClubMemberships m
|
||||||
|
WHERE m.IsDeleted = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = upp.TransactionId AND t.IsDeleted = 0
|
||||||
|
WHERE upp.UserId = m.UserId
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
AND (upp.TransactionId IS NULL OR t.PaymentStatus = 0)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
BestManual AS (
|
||||||
|
SELECT mp.UserId,
|
||||||
|
mp.Id AS ManualPaymentId,
|
||||||
|
mp.Amount,
|
||||||
|
mp.TransactionId,
|
||||||
|
COALESCE(mp.ApprovedAt, mp.Created) AS PurchasedAt,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY mp.UserId
|
||||||
|
ORDER BY COALESCE(mp.ApprovedAt, mp.Created) DESC, mp.Id DESC
|
||||||
|
) AS rn
|
||||||
|
FROM CMS.ManualPayments mp
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = mp.TransactionId AND t.IsDeleted = 0 AND t.PaymentStatus = 0
|
||||||
|
WHERE mp.IsDeleted = 0
|
||||||
|
AND mp.Status = 1 -- Approved
|
||||||
|
AND mp.TransactionId IS NOT NULL
|
||||||
|
),
|
||||||
|
BasePkg AS (
|
||||||
|
SELECT TOP (1) Id AS PackageId, Price
|
||||||
|
FROM CMS.Packages
|
||||||
|
WHERE IsDeleted = 0 AND IsBasePackage = 1
|
||||||
|
ORDER BY Id
|
||||||
|
)
|
||||||
|
INSERT INTO CMS.UserPackagePurchases
|
||||||
|
(
|
||||||
|
UserId, PackageId, PurchaseMethod, PurchasedAt, Amount,
|
||||||
|
OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
o.UserId,
|
||||||
|
COALESCE(o.PackageIdHint, b.PackageId),
|
||||||
|
3, -- Manual
|
||||||
|
bm.PurchasedAt,
|
||||||
|
bm.Amount,
|
||||||
|
NULL,
|
||||||
|
bm.TransactionId,
|
||||||
|
SYSUTCDATETIME(),
|
||||||
|
NULL,
|
||||||
|
SYSUTCDATETIME(),
|
||||||
|
NULL,
|
||||||
|
0
|
||||||
|
FROM OrphanClubUsers o
|
||||||
|
INNER JOIN BestManual bm ON bm.UserId = o.UserId AND bm.rn = 1
|
||||||
|
CROSS JOIN BasePkg b
|
||||||
|
WHERE COALESCE(o.PackageIdHint, b.PackageId) IS NOT NULL;
|
||||||
|
|
||||||
|
DECLARE @FromManual INT = @@ROWCOUNT;
|
||||||
|
PRINT CONCAT('Inserted from ManualPayments: ', @FromManual);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 2) Remaining orphans from Cycle / membership dates
|
||||||
|
------------------------------------------------------------
|
||||||
|
;WITH OrphanClubUsers AS (
|
||||||
|
SELECT
|
||||||
|
m.UserId,
|
||||||
|
m.PurchaseMethod,
|
||||||
|
u.PackagePurchaseMethod AS UserPurchaseMethod,
|
||||||
|
COALESCE(NULLIF(m.LastPackageId, 0), NULLIF(m.FirstPackageId, 0)) AS MembPackageId,
|
||||||
|
COALESCE(m.FirstActivationDate, m.LastActivationDate, m.ActivatedAt, m.Created) AS MembPurchasedAt
|
||||||
|
FROM CMS.ClubMemberships m
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = m.UserId AND u.IsDeleted = 0
|
||||||
|
WHERE m.IsDeleted = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = upp.TransactionId AND t.IsDeleted = 0
|
||||||
|
WHERE upp.UserId = m.UserId
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
AND (upp.TransactionId IS NULL OR t.PaymentStatus = 0)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
BestCycle AS (
|
||||||
|
SELECT
|
||||||
|
c.UserId,
|
||||||
|
c.PackageId,
|
||||||
|
c.PackageAmount,
|
||||||
|
c.PurchaseMethod,
|
||||||
|
c.PackagePurchasedAt,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY c.UserId ORDER BY c.CycleNumber ASC, c.Id ASC) AS rn
|
||||||
|
FROM CMS.ClubMembershipCycles c
|
||||||
|
WHERE c.IsDeleted = 0
|
||||||
|
),
|
||||||
|
BasePkg AS (
|
||||||
|
SELECT TOP (1) Id AS PackageId, Price
|
||||||
|
FROM CMS.Packages
|
||||||
|
WHERE IsDeleted = 0 AND IsBasePackage = 1
|
||||||
|
ORDER BY Id
|
||||||
|
),
|
||||||
|
ToInsert AS (
|
||||||
|
SELECT
|
||||||
|
o.UserId,
|
||||||
|
COALESCE(NULLIF(bc.PackageId, 0), o.MembPackageId, b.PackageId) AS PackageId,
|
||||||
|
CASE
|
||||||
|
WHEN bc.PurchaseMethod IS NOT NULL AND bc.PurchaseMethod <> 0 THEN bc.PurchaseMethod
|
||||||
|
WHEN o.PurchaseMethod IS NOT NULL AND o.PurchaseMethod <> 0 THEN o.PurchaseMethod
|
||||||
|
WHEN o.UserPurchaseMethod IS NOT NULL AND o.UserPurchaseMethod <> 0 THEN o.UserPurchaseMethod
|
||||||
|
ELSE 3 -- Manual (last resort)
|
||||||
|
END AS PurchaseMethod,
|
||||||
|
COALESCE(bc.PackagePurchasedAt, o.MembPurchasedAt, SYSUTCDATETIME()) AS PurchasedAt,
|
||||||
|
CASE
|
||||||
|
WHEN bc.PackageAmount IS NOT NULL AND bc.PackageAmount > 0 THEN bc.PackageAmount
|
||||||
|
ELSE COALESCE(p.Price, b.Price, 0)
|
||||||
|
END AS Amount
|
||||||
|
FROM OrphanClubUsers o
|
||||||
|
LEFT JOIN BestCycle bc ON bc.UserId = o.UserId AND bc.rn = 1
|
||||||
|
CROSS JOIN BasePkg b
|
||||||
|
LEFT JOIN CMS.Packages p ON p.Id = COALESCE(NULLIF(bc.PackageId, 0), o.MembPackageId, b.PackageId) AND p.IsDeleted = 0
|
||||||
|
)
|
||||||
|
SELECT *
|
||||||
|
INTO #BackfillCandidates
|
||||||
|
FROM ToInsert
|
||||||
|
WHERE PackageId IS NOT NULL AND Amount > 0;
|
||||||
|
|
||||||
|
DECLARE @Remaining INT = (SELECT COUNT(*) FROM #BackfillCandidates);
|
||||||
|
PRINT CONCAT('Remaining orphans to synthetic backfill: ', @Remaining);
|
||||||
|
|
||||||
|
DECLARE @UserId BIGINT, @PackageId BIGINT, @PurchaseMethod INT, @PurchasedAt DATETIME2, @Amount BIGINT;
|
||||||
|
DECLARE @TxId BIGINT;
|
||||||
|
|
||||||
|
DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
|
||||||
|
SELECT UserId, PackageId, PurchaseMethod, PurchasedAt, Amount
|
||||||
|
FROM #BackfillCandidates;
|
||||||
|
|
||||||
|
OPEN cur;
|
||||||
|
FETCH NEXT FROM cur INTO @UserId, @PackageId, @PurchaseMethod, @PurchasedAt, @Amount;
|
||||||
|
|
||||||
|
DECLARE @Synthetic INT = 0;
|
||||||
|
|
||||||
|
WHILE @@FETCH_STATUS = 0
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.Transactions
|
||||||
|
(
|
||||||
|
Amount, Description, PaymentStatus, PaymentDate, RefId, Type,
|
||||||
|
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@Amount,
|
||||||
|
CONCAT(N'بکفیل عضویت باشگاه — UserId ', @UserId),
|
||||||
|
0, -- Success
|
||||||
|
@PurchasedAt,
|
||||||
|
CONCAT('UPP-BACKFILL-', @UserId, '-', @PackageId),
|
||||||
|
2, -- DepositExternal1
|
||||||
|
SYSUTCDATETIME(), NULL, SYSUTCDATETIME(), NULL, 0
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @TxId = SCOPE_IDENTITY();
|
||||||
|
|
||||||
|
INSERT INTO CMS.UserPackagePurchases
|
||||||
|
(
|
||||||
|
UserId, PackageId, PurchaseMethod, PurchasedAt, Amount,
|
||||||
|
OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@UserId, @PackageId, @PurchaseMethod, @PurchasedAt, @Amount,
|
||||||
|
NULL, @TxId, SYSUTCDATETIME(), NULL, SYSUTCDATETIME(), NULL, 0
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @Synthetic += 1;
|
||||||
|
FETCH NEXT FROM cur INTO @UserId, @PackageId, @PurchaseMethod, @PurchasedAt, @Amount;
|
||||||
|
END
|
||||||
|
|
||||||
|
CLOSE cur;
|
||||||
|
DEALLOCATE cur;
|
||||||
|
|
||||||
|
PRINT CONCAT('Inserted synthetic UPP rows: ', @Synthetic);
|
||||||
|
|
||||||
|
DROP TABLE #BackfillCandidates;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Verify invariant
|
||||||
|
------------------------------------------------------------
|
||||||
|
DECLARE @ClubCount INT =
|
||||||
|
(
|
||||||
|
SELECT COUNT(*) FROM CMS.ClubMemberships WHERE IsDeleted = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE @OrphansLeft INT =
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM CMS.ClubMemberships m
|
||||||
|
WHERE m.IsDeleted = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = upp.TransactionId AND t.IsDeleted = 0
|
||||||
|
WHERE upp.UserId = m.UserId
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
AND (upp.TransactionId IS NULL OR t.PaymentStatus = 0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE @SuccessfulBuyers INT =
|
||||||
|
(
|
||||||
|
SELECT COUNT(DISTINCT upp.UserId)
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = upp.TransactionId AND t.IsDeleted = 0
|
||||||
|
WHERE upp.IsDeleted = 0
|
||||||
|
AND (upp.TransactionId IS NULL OR t.PaymentStatus = 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
PRINT CONCAT('ClubMemberships: ', @ClubCount);
|
||||||
|
PRINT CONCAT('Distinct successful UPP users: ', @SuccessfulBuyers);
|
||||||
|
PRINT CONCAT('Orphans left (must be 0): ', @OrphansLeft);
|
||||||
|
|
||||||
|
IF @OrphansLeft > 0
|
||||||
|
BEGIN
|
||||||
|
ROLLBACK;
|
||||||
|
THROW 50001, 'Backfill incomplete — orphans remain; transaction rolled back.', 1;
|
||||||
|
END
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
PRINT 'Backfill committed successfully.';
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
/*
|
||||||
|
DIAGNOSTIC ONLY — maps RefId LIKE 'manualy%' transactions to Users / WalletHistory.
|
||||||
|
|
||||||
|
Background (ChargeUserWallets.sql):
|
||||||
|
RefId = 'manualy' + ChangeLogId at charge time (original UserWalletChangeLogs id).
|
||||||
|
After migration/rebuild, WH ids shifted; RefrenceId on some rows stayed 0.
|
||||||
|
When RefrenceId=0, CreatedBy may store the Transaction Id as a fallback hint —
|
||||||
|
but Tx may never have been created; do NOT infer Tx from mobile or manualy suffix alone.
|
||||||
|
|
||||||
|
Rules used here:
|
||||||
|
- WH.RefrenceId > 0 → trusted link to Tx
|
||||||
|
- WH.RefrenceId = 0 → Tx ONLY if CreatedBy is numeric AND that Tx exists with RefId manualy%
|
||||||
|
- Tx → User via mobile (CreatedBy/LastModifiedBy) is informational, not used alone for Ref=0 WH
|
||||||
|
- manualy{N} suffix N = original ChangeLog id (may differ from current WH.Id)
|
||||||
|
|
||||||
|
Run on staging before script 6.
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @ManualyTx TABLE (
|
||||||
|
TransactionId BIGINT PRIMARY KEY,
|
||||||
|
RefId NVARCHAR(100) NOT NULL,
|
||||||
|
OriginalChangeLogId BIGINT NULL,
|
||||||
|
Amount BIGINT,
|
||||||
|
TxCreatedBy NVARCHAR(200),
|
||||||
|
TxLastModifiedBy NVARCHAR(200),
|
||||||
|
PaymentDate DATETIME2
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO @ManualyTx (TransactionId, RefId, OriginalChangeLogId, Amount, TxCreatedBy, TxLastModifiedBy, PaymentDate)
|
||||||
|
SELECT
|
||||||
|
t.Id,
|
||||||
|
t.RefId,
|
||||||
|
TRY_CAST(
|
||||||
|
SUBSTRING(t.RefId, LEN(N'manualy') + 1, 20)
|
||||||
|
AS BIGINT
|
||||||
|
),
|
||||||
|
t.Amount,
|
||||||
|
t.CreatedBy,
|
||||||
|
t.LastModifiedBy,
|
||||||
|
t.PaymentDate
|
||||||
|
FROM CMS.Transactions t
|
||||||
|
WHERE t.IsDeleted = 0
|
||||||
|
AND t.RefId LIKE N'manualy%';
|
||||||
|
|
||||||
|
DECLARE @ManualyTxCount INT = (SELECT COUNT(*) FROM @ManualyTx);
|
||||||
|
PRINT CONCAT('manualy transactions in DB: ', @ManualyTxCount);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 1) Each manualy Tx → User(s) via WH.RefrenceId / CreatedBy hint / mobile
|
||||||
|
------------------------------------------------------------
|
||||||
|
PRINT '=== 1) manualy Tx → User resolution ===';
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
mt.TransactionId,
|
||||||
|
mt.RefId,
|
||||||
|
mt.OriginalChangeLogId,
|
||||||
|
mt.Amount,
|
||||||
|
mt.PaymentDate,
|
||||||
|
mt.TxCreatedBy,
|
||||||
|
mt.TxLastModifiedBy,
|
||||||
|
u_mobile.Id AS UserByMobile,
|
||||||
|
u_mobile.Mobile AS UserByMobilePhone,
|
||||||
|
u_mobile.FirstName AS UserByMobileFirst,
|
||||||
|
u_mobile.LastName AS UserByMobileLast,
|
||||||
|
h_linked.Id AS WhIdViaRef,
|
||||||
|
h_linked.WalletId AS WhWalletViaRef,
|
||||||
|
u_wh.Id AS UserViaWhRef,
|
||||||
|
h_hint.Id AS WhIdViaCreatedByHint,
|
||||||
|
h_hint.WalletId AS WhWalletViaHint,
|
||||||
|
u_hint.Id AS UserViaWhHint,
|
||||||
|
CASE
|
||||||
|
WHEN u_wh.Id IS NOT NULL THEN N'WH_REFRENCE_ID'
|
||||||
|
WHEN u_hint.Id IS NOT NULL THEN N'WH_CREATEDBY_HINT'
|
||||||
|
WHEN u_mobile.Id IS NOT NULL THEN N'TX_MOBILE_ONLY'
|
||||||
|
ELSE N'UNRESOLVED'
|
||||||
|
END AS PrimaryLink,
|
||||||
|
CASE
|
||||||
|
WHEN u_wh.Id IS NOT NULL AND u_hint.Id IS NOT NULL AND u_wh.Id <> u_hint.Id
|
||||||
|
THEN N'CONFLICT_WH_REF_VS_HINT'
|
||||||
|
WHEN u_wh.Id IS NOT NULL AND u_mobile.Id IS NOT NULL AND u_wh.Id <> u_mobile.Id
|
||||||
|
THEN N'CONFLICT_WH_REF_VS_MOBILE'
|
||||||
|
WHEN u_wh.Id IS NULL AND u_hint.Id IS NULL AND u_mobile.Id IS NULL
|
||||||
|
THEN N'NO_USER_FOUND'
|
||||||
|
ELSE N'OK'
|
||||||
|
END AS LinkQuality
|
||||||
|
FROM @ManualyTx mt
|
||||||
|
LEFT JOIN CMS.Users u_mobile ON u_mobile.IsDeleted = 0
|
||||||
|
AND u_mobile.Mobile IN (
|
||||||
|
NULLIF(LTRIM(RTRIM(mt.TxCreatedBy)), N''),
|
||||||
|
NULLIF(LTRIM(RTRIM(mt.TxLastModifiedBy)), N'')
|
||||||
|
)
|
||||||
|
LEFT JOIN CMS.UserWalletHistories h_linked ON h_linked.IsDeleted = 0
|
||||||
|
AND h_linked.RefrenceId = mt.TransactionId
|
||||||
|
LEFT JOIN CMS.UserWallets w_linked ON w_linked.Id = h_linked.WalletId AND w_linked.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Users u_wh ON u_wh.Id = w_linked.UserId AND u_wh.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWalletHistories h_hint ON h_hint.IsDeleted = 0
|
||||||
|
AND (h_hint.RefrenceId IS NULL OR h_hint.RefrenceId = 0)
|
||||||
|
AND TRY_CAST(h_hint.CreatedBy AS BIGINT) = mt.TransactionId
|
||||||
|
LEFT JOIN CMS.UserWallets w_hint ON w_hint.Id = h_hint.WalletId AND w_hint.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Users u_hint ON u_hint.Id = w_hint.UserId AND u_hint.IsDeleted = 0
|
||||||
|
ORDER BY mt.TransactionId;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 2) WH rows with RefrenceId=0 — CreatedBy hint analysis
|
||||||
|
------------------------------------------------------------
|
||||||
|
PRINT '=== 2) WalletHistory RefrenceId=0 (manualy charge candidates) ===';
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
h.Id AS WalletHistoryId,
|
||||||
|
h.WalletId,
|
||||||
|
w.UserId AS WalletUserId,
|
||||||
|
u.Mobile AS WalletUserMobile,
|
||||||
|
h.RefrenceId AS WhRefrenceId,
|
||||||
|
h.ChangeValue,
|
||||||
|
h.Created AS WhCreated,
|
||||||
|
h.CreatedBy AS WhCreatedBy,
|
||||||
|
h.LastModifiedBy AS WhLastModifiedBy,
|
||||||
|
t_hint.Id AS TxByCreatedByHint,
|
||||||
|
t_hint.RefId AS TxHintRefId,
|
||||||
|
t_hint.Amount AS TxHintAmount,
|
||||||
|
u_hint_mobile.Id AS TxHintUserByMobile,
|
||||||
|
CASE
|
||||||
|
WHEN TRY_CAST(h.CreatedBy AS BIGINT) IS NULL OR TRY_CAST(h.CreatedBy AS BIGINT) = 0
|
||||||
|
THEN N'NO_NUMERIC_HINT'
|
||||||
|
WHEN t_hint.Id IS NULL
|
||||||
|
THEN N'HINT_POINTS_TO_MISSING_TX'
|
||||||
|
WHEN t_hint.RefId NOT LIKE N'manualy%'
|
||||||
|
THEN N'HINT_TX_NOT_MANUALY'
|
||||||
|
WHEN w.UserId = u_hint_mobile.Id
|
||||||
|
THEN N'FIX_LINK_WH_TO_HINT_TX'
|
||||||
|
WHEN u_hint_mobile.Id IS NOT NULL AND w.UserId <> u_hint_mobile.Id
|
||||||
|
THEN N'HINT_TX_USER_MISMATCH_WALLET'
|
||||||
|
ELSE N'FIX_LINK_REVIEW_MOBILE'
|
||||||
|
END AS WhZeroRefVerdict,
|
||||||
|
CASE
|
||||||
|
WHEN t_hint.Id IS NOT NULL AND t_hint.RefId LIKE N'manualy%'
|
||||||
|
THEN CONCAT(N'UPDATE WH ', h.Id, N' SET RefrenceId=', t_hint.Id)
|
||||||
|
WHEN TRY_CAST(h.CreatedBy AS BIGINT) IS NULL OR TRY_CAST(h.CreatedBy AS BIGINT) = 0
|
||||||
|
THEN N'Cannot auto-link — no CreatedBy Tx hint'
|
||||||
|
ELSE N'Tx from hint missing — may need manual charge review'
|
||||||
|
END AS SuggestedAction
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Users u ON u.Id = w.UserId AND u.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Transactions t_hint ON t_hint.Id = TRY_CAST(h.CreatedBy AS BIGINT)
|
||||||
|
AND t_hint.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Users u_hint_mobile ON u_hint_mobile.IsDeleted = 0
|
||||||
|
AND u_hint_mobile.Mobile IN (
|
||||||
|
NULLIF(LTRIM(RTRIM(t_hint.CreatedBy)), N''),
|
||||||
|
NULLIF(LTRIM(RTRIM(t_hint.LastModifiedBy)), N'')
|
||||||
|
)
|
||||||
|
WHERE h.IsDeleted = 0
|
||||||
|
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
|
||||||
|
AND (
|
||||||
|
TRY_CAST(h.CreatedBy AS BIGINT) IS NOT NULL
|
||||||
|
OR h.LastModifiedBy LIKE N'09%'
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM @ManualyTx mt
|
||||||
|
WHERE mt.OriginalChangeLogId = h.Id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ORDER BY h.Id;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 3) manualy suffix vs current WH.Id (id-shift detector)
|
||||||
|
------------------------------------------------------------
|
||||||
|
PRINT '=== 3) manualy suffix N vs current WH.Id (id shift) ===';
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
mt.TransactionId,
|
||||||
|
mt.RefId,
|
||||||
|
mt.OriginalChangeLogId AS ManualySuffixN,
|
||||||
|
h_by_suffix.Id AS CurrentWhIdWithSameIdAsSuffix,
|
||||||
|
h_by_suffix.RefrenceId AS ThatWhRefrenceId,
|
||||||
|
h_by_suffix.WalletId,
|
||||||
|
CASE
|
||||||
|
WHEN h_by_suffix.Id IS NULL THEN N'SUFFIX_WH_ID_GONE'
|
||||||
|
WHEN h_by_suffix.RefrenceId = mt.TransactionId THEN N'SUFFIX_WH_LINKED_OK'
|
||||||
|
WHEN h_by_suffix.RefrenceId IS NULL OR h_by_suffix.RefrenceId = 0
|
||||||
|
THEN N'SUFFIX_WH_EXISTS_BROKEN_REF'
|
||||||
|
ELSE N'SUFFIX_WH_POINTS_OTHER_TX'
|
||||||
|
END AS SuffixMatchStatus
|
||||||
|
FROM @ManualyTx mt
|
||||||
|
LEFT JOIN CMS.UserWalletHistories h_by_suffix ON h_by_suffix.Id = mt.OriginalChangeLogId
|
||||||
|
AND h_by_suffix.IsDeleted = 0
|
||||||
|
ORDER BY mt.TransactionId;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 4) Distinct users in scope for script 6 (manualy-linked only)
|
||||||
|
------------------------------------------------------------
|
||||||
|
PRINT '=== 4) Users in manualy fix scope ===';
|
||||||
|
|
||||||
|
;WITH ScopeUsers AS (
|
||||||
|
SELECT DISTINCT w.UserId, mt.TransactionId, N'WH_REF' AS Source
|
||||||
|
FROM @ManualyTx mt
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.RefrenceId = mt.TransactionId AND h.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
SELECT DISTINCT w.UserId, t.Id, N'WH_CREATEDBY_HINT'
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
|
||||||
|
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
|
||||||
|
WHERE h.IsDeleted = 0
|
||||||
|
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
|
||||||
|
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
SELECT DISTINCT u.Id, mt.TransactionId, N'TX_MOBILE'
|
||||||
|
FROM @ManualyTx mt
|
||||||
|
INNER JOIN CMS.Users u ON u.IsDeleted = 0
|
||||||
|
AND u.Mobile IN (
|
||||||
|
NULLIF(LTRIM(RTRIM(mt.TxCreatedBy)), N''),
|
||||||
|
NULLIF(LTRIM(RTRIM(mt.TxLastModifiedBy)), N'')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
su.UserId,
|
||||||
|
u.Mobile,
|
||||||
|
u.FirstName,
|
||||||
|
u.LastName,
|
||||||
|
u.PackagePurchaseMethod,
|
||||||
|
MIN(su.TransactionId) AS SampleManualyTxId,
|
||||||
|
STRING_AGG(su.Source, N', ') WITHIN GROUP (ORDER BY su.Source) AS LinkSources,
|
||||||
|
CASE WHEN EXISTS (
|
||||||
|
SELECT 1 FROM CMS.Transactions t2
|
||||||
|
WHERE t2.IsDeleted = 0 AND t2.RefId LIKE N'C4%'
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM CMS.Users ux
|
||||||
|
WHERE ux.Id = su.UserId AND ux.IsDeleted = 0
|
||||||
|
AND ux.Mobile IN (NULLIF(t2.CreatedBy,N''), NULLIF(t2.LastModifiedBy,N''))
|
||||||
|
)
|
||||||
|
) THEN 1 ELSE 0 END AS AlsoHasC4Tx,
|
||||||
|
(SELECT COUNT(*) FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = su.UserId AND upp.IsDeleted = 0) AS UppCount
|
||||||
|
FROM ScopeUsers su
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = su.UserId AND u.IsDeleted = 0
|
||||||
|
GROUP BY su.UserId, u.Mobile, u.FirstName, u.LastName, u.PackagePurchaseMethod
|
||||||
|
ORDER BY su.UserId;
|
||||||
|
|
||||||
|
PRINT 'Done (diagnostic only — no changes).';
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"TransactionId","RefId","OriginalChangeLogId","Amount","PaymentDate","TxCreatedBy","TxLastModifiedBy","UserByMobile","UserByMobilePhone","UserByMobileFirst","UserByMobileLast","WhIdViaRef","WhWalletViaRef","UserViaWhRef","WhIdViaCreatedByHint","WhWalletViaHint","UserViaWhHint","PrimaryLink","LinkQuality"
|
||||||
|
"23","manualy24","24","56000000","19/11/2025 01:24:24",,,,,,,"102","38","75",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"23","manualy24","24","56000000","19/11/2025 01:24:24",,,,,,,"216","24","47",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"24","manualy25","25","56000000","19/11/2025 01:24:24",,,,,,,"103","46","93",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"24","manualy25","25","56000000","19/11/2025 01:24:24",,,,,,,"217","6","50",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"25","manualy26","26","56000000","19/11/2025 01:24:24",,,,,,,"104","47","96","218","34","71","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
|
||||||
|
"26","manualy27","27","56000000","19/11/2025 01:24:24",,,,,,,"105","49","99","219","37","74","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
|
||||||
|
"28","manualy29","29","56000000","19/11/2025 01:24:24",,,,,,,"106","24","47",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"28","manualy29","29","56000000","19/11/2025 01:24:24",,,,,,,"221","42","88",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"29","manualy30","30","56000000","19/11/2025 01:24:24",,,,,,,"107","22","43",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"29","manualy30","30","56000000","19/11/2025 01:24:24",,,,,,,"222","62","114",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"30","manualy31","31","56000000","19/11/2025 01:24:24",,,,,,,"108","23","45",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"30","manualy31","31","56000000","19/11/2025 01:24:24",,,,,,,"223","13","7",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"31","manualy32","32","56000000","28/11/2025 05:27:38",,,,,,,"122","26","52",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"31","manualy32","32","56000000","28/11/2025 05:27:38",,,,,,,"224","15","9",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"42","manualy43","43","56000000","3/12/2025 09:49:45.92","09014638673","09014638673","116","09014638673","سالار","رضائی","138","64","116",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"42","manualy43","43","56000000","3/12/2025 09:49:45.92","09014638673","09014638673","116","09014638673","سالار","رضائی","235","24","47",,,,"WH_REFRENCE_ID","CONFLICT_WH_REF_VS_MOBILE"
|
||||||
|
"43","manualy44","44","56000000","3/12/2025 09:49:45.92","09056365323","09056365323","115","09056365323","حامد","دهقانی","139","63","115","236","32","69","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
|
||||||
|
"44","manualy45","45","56000000","3/12/2025 09:49:45.92","09126092994","09126092994","119","09126092994","علیرضا ","کریمی پیروز ","140","67","119",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"44","manualy45","45","56000000","3/12/2025 09:49:45.92","09126092994","09126092994","119","09126092994","علیرضا ","کریمی پیروز ","237","38","75",,,,"WH_REFRENCE_ID","CONFLICT_WH_REF_VS_MOBILE"
|
||||||
|
"45","manualy46","46","56000000","3/12/2025 09:49:45.92","09177325630","09177325630","113","09177325630","کاوس","بیگ اینالو","141","61","113",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"45","manualy46","46","56000000","3/12/2025 09:49:45.92","09177325630","09177325630","113","09177325630","کاوس","بیگ اینالو","238","6","50",,,,"WH_REFRENCE_ID","CONFLICT_WH_REF_VS_MOBILE"
|
||||||
|
"46","manualy47","47","56000000","3/12/2025 09:49:45.92","09384366040","09384366040","103","09384366040","علی ","دلال زاده","142","51","103",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"46","manualy47","47","56000000","3/12/2025 09:49:45.92","09384366040","09384366040","103","09384366040","علی ","دلال زاده","239","13","7",,,,"WH_REFRENCE_ID","CONFLICT_WH_REF_VS_MOBILE"
|
||||||
|
"56","manualy57","57","56000000","3/12/2025 09:49:45.92","09015793338","09015793338","71","09015793338","حمیدرضا","مرجانی","249","62","114","118","34","71","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
|
||||||
|
"57","manualy58","58","56000000","19/11/2025 01:24:24","09961354257","09961354257",,,,,"250","13","7","124","35","72","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
|
||||||
|
"58","manualy59","59","56000000","19/11/2025 01:24:24","09217672935","09217672935","69","09217672935","مهدی ","گیوی ","251","15","9","119","32","69","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
|
||||||
|
"59","manualy60","60","56000000","19/11/2025 01:24:24","09101692049","09101692049","74","09101692049","سجاد ","اداوی","252","22","43","120","37","74","WH_REFRENCE_ID","CONFLICT_WH_REF_VS_HINT"
|
||||||
|
"68","manualy69","69","56000000","8/12/2025 12:30:53.536666","09399172812","09399172812","142","09399172812","صغری","شبانکاره ",,,,,,,"TX_MOBILE_ONLY","OK"
|
||||||
|
"107","manualy292","292","56000000","30/12/2025 01:24:24",,,,,,,"186","15","9",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"107","manualy292","292","56000000","30/12/2025 01:24:24",,,,,,,"292","118","170",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"107","manualy292","292","56000000","30/12/2025 01:24:24",,,,,,,"40399","34","71",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"108","manualy293","293","56000000","31/12/2025 01:24:24",,,,,,,"187","22","43",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"108","manualy293","293","56000000","31/12/2025 01:24:24",,,,,,,"293","119","171",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"108","manualy293","293","56000000","31/12/2025 01:24:24",,,,,,,"40400","38","75",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"109","manualy294","294","56000000","31/12/2025 01:24:24",,,,,,,"188","23","45",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"109","manualy294","294","56000000","31/12/2025 01:24:24",,,,,,,"294","120","172",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"109","manualy294","294","56000000","31/12/2025 01:24:24",,,,,,,"40401","59","111",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"110","manualy295","295","56000000","1/1/2026 17:07:53.2301978",,,,,,,"189","24","47",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"110","manualy295","295","56000000","1/1/2026 17:07:53.2301978",,,,,,,"295","123","175",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"110","manualy295","295","56000000","1/1/2026 17:07:53.2301978",,,,,,,"40402","134","186",,,,"WH_REFRENCE_ID","OK"
|
||||||
|
"118","34","71","09015793338","0","1","3/12/2025 09:49:45.92","56","09015793338","56","manualy57","56000000","71","FIX_LINK_WH_TO_HINT_TX","UPDATE WH 118 SET RefrenceId=56"
|
||||||
|
"119","32","69","09217672935","0","1","19/11/2025 01:24:24","58","09217672935","58","manualy59","56000000","69","FIX_LINK_WH_TO_HINT_TX","UPDATE WH 119 SET RefrenceId=58"
|
||||||
|
"120","37","74","09101692049","0","1","19/11/2025 01:24:24","59","09101692049","59","manualy60","56000000","74","FIX_LINK_WH_TO_HINT_TX","UPDATE WH 120 SET RefrenceId=59"
|
||||||
|
"121","12","2","09387342688","0","1","19/12/2025 22:00:40.276666","0","09387342688",,,,,"NO_NUMERIC_HINT","Cannot auto-link — no CreatedBy Tx hint"
|
||||||
|
"124","35","72","09124678697","0","1","19/11/2025 01:24:24","57","09961354257","57","manualy58","56000000",,"FIX_LINK_REVIEW_MOBILE","UPDATE WH 124 SET RefrenceId=57"
|
||||||
|
"201","110","162","09199877503","0","1","20/12/2025 02:24:42.771904","92","162",,,,,"HINT_POINTS_TO_MISSING_TX","Tx from hint missing — may need manual charge review"
|
||||||
|
"209","32","69","09217672935","0","0","20/12/2025 10:30:49.186666","16","SP","16","localwallet-d0870aee-8a21-4cfc-a5a0-362b25795af0","10000",,"HINT_TX_NOT_MANUALY","Tx from hint missing — may need manual charge review"
|
||||||
|
"218","34","71","09015793338","0","0","20/12/2025 10:31:12.52","25","SP","25","manualy26","56000000",,"FIX_LINK_REVIEW_MOBILE","UPDATE WH 218 SET RefrenceId=25"
|
||||||
|
"219","37","74","09101692049","0","0","20/12/2025 10:31:12.52","26","SP","26","manualy27","56000000",,"FIX_LINK_REVIEW_MOBILE","UPDATE WH 219 SET RefrenceId=26"
|
||||||
|
"236","32","69","09217672935","0","0","20/12/2025 10:38:43.333333","43","SP","43","manualy44","56000000","115","HINT_TX_USER_MISMATCH_WALLET","UPDATE WH 236 SET RefrenceId=43"
|
||||||
|
"245","34","71","09015793338","0","0","20/12/2025 10:39:58.833333","52","SP","52","localwallet-533cd27c-9ff5-497b-b23d-68df374c1107","37035000",,"HINT_TX_NOT_MANUALY","Tx from hint missing — may need manual charge review"
|
||||||
|
"246","37","74","09101692049","0","0","20/12/2025 10:39:58.833333","53","SP","53","C4_N9894294","56000000","88","HINT_TX_NOT_MANUALY","Tx from hint missing — may need manual charge review"
|
||||||
|
"23","manualy24","24",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"24","manualy25","25",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"25","manualy26","26",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"26","manualy27","27",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"28","manualy29","29",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"29","manualy30","30",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"30","manualy31","31",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"31","manualy32","32",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"42","manualy43","43",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"43","manualy44","44",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"44","manualy45","45",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"45","manualy46","46",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"46","manualy47","47",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"56","manualy57","57",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"57","manualy58","58",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"58","manualy59","59",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"59","manualy60","60",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"68","manualy69","69",,,,"SUFFIX_WH_ID_GONE"
|
||||||
|
"107","manualy292","292","292","107","118","SUFFIX_WH_LINKED_OK"
|
||||||
|
"108","manualy293","293","293","108","119","SUFFIX_WH_LINKED_OK"
|
||||||
|
"109","manualy294","294","294","109","120","SUFFIX_WH_LINKED_OK"
|
||||||
|
"110","manualy295","295","295","110","123","SUFFIX_WH_LINKED_OK"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","30","WH_REF, WH_REF, WH_REF","0","0"
|
||||||
|
"9","09387095611","مهدی","مرجانی","3","31","WH_REF, WH_REF, WH_REF","0","0"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","29","WH_REF, WH_REF, WH_REF","0","0"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","30","WH_REF, WH_REF","0","0"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","23","WH_REF, WH_REF, WH_REF, WH_REF","0","0"
|
||||||
|
"50","09207316560","مرتضی ","اینالو","1","24","WH_REF, WH_REF","0","1"
|
||||||
|
"52","09178327020","حمیدرضا ","اسمعیلی ","3","31","WH_REF","0","0"
|
||||||
|
"69","09217672935","مهدی ","گیوی ","3","43","TX_MOBILE, WH_CREATEDBY_HINT, WH_CREATEDBY_HINT","0","0"
|
||||||
|
"71","09015793338","حمیدرضا","مرجانی","3","25","TX_MOBILE, WH_CREATEDBY_HINT, WH_CREATEDBY_HINT, WH_REF","0","0"
|
||||||
|
"72","09124678697","مژگان","گیوی","3","57","WH_CREATEDBY_HINT","0","0"
|
||||||
|
"74","09101692049","سجاد ","اداوی","3","26","TX_MOBILE, WH_CREATEDBY_HINT, WH_CREATEDBY_HINT","0","0"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","23","WH_REF, WH_REF, WH_REF","0","0"
|
||||||
|
"88","09172606097","کریم","رعیت پیشه","1","28","WH_REF","1","0"
|
||||||
|
"93","09122156286","ریحانه سادات ","هاشمی نصر ","3","24","WH_REF","0","0"
|
||||||
|
"96","09125020729","غلامرضا ","عبدی ","3","25","WH_REF","0","0"
|
||||||
|
"99","09106519822","هستی","خادمی","3","26","WH_REF","0","0"
|
||||||
|
"103","09384366040","علی ","دلال زاده","3","46","TX_MOBILE, WH_REF","0","0"
|
||||||
|
"111","09352828086","انسیه","توسلیان","1","109","WH_REF","0","0"
|
||||||
|
"113","09177325630","کاوس","بیگ اینالو","3","45","TX_MOBILE, WH_REF","0","0"
|
||||||
|
"114","09397939699","اکرم","دهقانی","1","29","WH_REF, WH_REF","1","0"
|
||||||
|
"115","09056365323","حامد","دهقانی","3","43","TX_MOBILE, WH_REF","0","0"
|
||||||
|
"116","09014638673","سالار","رضائی","3","42","TX_MOBILE, WH_REF","0","0"
|
||||||
|
"119","09126092994","علیرضا ","کریمی پیروز ","3","44","TX_MOBILE, WH_REF","0","0"
|
||||||
|
"142","09399172812","صغری","شبانکاره ","3","68","TX_MOBILE","0","0"
|
||||||
|
"170","09120991323","لیلا","خدارحمی دره میانه","3","107","WH_REF","0","0"
|
||||||
|
"171","09375724408","مژده","بنی هاشمی","3","108","WH_REF","0","0"
|
||||||
|
"172","09121355832","مهرافشان","زاهدنیا","3","109","WH_REF","0","0"
|
||||||
|
"175","09122332219","ناهید ","حسن زاده مژدهی","3","110","WH_REF","0","0"
|
||||||
|
"186","09395618852","احمد","عالم","2","110","WH_REF","0","2"
|
||||||
|
@@ -0,0 +1,90 @@
|
|||||||
|
-- =============================================
|
||||||
|
-- Diagnose: Leg counts — all subtree vs active-only vs gold/silver
|
||||||
|
-- Usage: SET @RootUserId to the tree owner UserId
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
DECLARE @RootUserId BIGINT = 1; -- ← change to target user
|
||||||
|
|
||||||
|
DECLARE @GoldPackageId BIGINT = (
|
||||||
|
SELECT TOP 1 Id FROM CMS.Packages
|
||||||
|
WHERE IsDeleted = 0 AND IsActive = 1 AND IsBasePackage = 1
|
||||||
|
ORDER BY Id
|
||||||
|
);
|
||||||
|
DECLARE @SilverPackageId BIGINT = (
|
||||||
|
SELECT TOP 1 Id FROM CMS.Packages
|
||||||
|
WHERE IsDeleted = 0 AND IsActive = 1 AND IsBasePackage = 0
|
||||||
|
AND (Title LIKE N'%نقر%' OR MaxBalancesPerLeg <= 30)
|
||||||
|
ORDER BY Id
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE @LeftChildId BIGINT = (
|
||||||
|
SELECT Id FROM CMS.Users
|
||||||
|
WHERE NetworkParentId = @RootUserId AND LegPosition = 0 AND IsDeleted = 0
|
||||||
|
);
|
||||||
|
DECLARE @RightChildId BIGINT = (
|
||||||
|
SELECT Id FROM CMS.Users
|
||||||
|
WHERE NetworkParentId = @RootUserId AND LegPosition = 1 AND IsDeleted = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
;WITH LegUsers AS (
|
||||||
|
SELECT @LeftChildId AS RootLegUserId, 0 AS LegSide
|
||||||
|
WHERE @LeftChildId IS NOT NULL
|
||||||
|
UNION ALL
|
||||||
|
SELECT @RightChildId, 1
|
||||||
|
WHERE @RightChildId IS NOT NULL
|
||||||
|
),
|
||||||
|
Subtree AS (
|
||||||
|
SELECT lu.RootLegUserId, lu.LegSide, lu.RootLegUserId AS UserId
|
||||||
|
FROM LegUsers lu
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT s.RootLegUserId, s.LegSide, u.Id
|
||||||
|
FROM Subtree s
|
||||||
|
INNER JOIN CMS.Users u ON u.NetworkParentId = s.UserId AND u.IsDeleted = 0
|
||||||
|
),
|
||||||
|
AllSubtreeCounts AS (
|
||||||
|
SELECT LegSide, COUNT(DISTINCT UserId) AS Cnt
|
||||||
|
FROM Subtree
|
||||||
|
GROUP BY LegSide
|
||||||
|
),
|
||||||
|
ActiveClubCounts AS (
|
||||||
|
SELECT s.LegSide, COUNT(DISTINCT s.UserId) AS Cnt
|
||||||
|
FROM Subtree s
|
||||||
|
INNER JOIN CMS.ClubMemberships cm
|
||||||
|
ON cm.UserId = s.UserId AND cm.IsActive = 1 AND cm.IsDeleted = 0
|
||||||
|
GROUP BY s.LegSide
|
||||||
|
),
|
||||||
|
ActivePackageHolders AS (
|
||||||
|
SELECT
|
||||||
|
s.LegSide,
|
||||||
|
cc.PackageId,
|
||||||
|
COUNT(DISTINCT s.UserId) AS HolderCount
|
||||||
|
FROM Subtree s
|
||||||
|
INNER JOIN CMS.ClubMemberships cm ON cm.UserId = s.UserId AND cm.IsActive = 1 AND cm.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.ClubMembershipCycles cc ON cc.UserId = s.UserId AND cc.IsCurrentCycle = 1
|
||||||
|
GROUP BY s.LegSide, cc.PackageId
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
@RootUserId AS RootUserId,
|
||||||
|
-- All subtree (old سازمان چپ/راست)
|
||||||
|
ISNULL((SELECT Cnt FROM AllSubtreeCounts WHERE LegSide = 0), 0) AS AllLeft,
|
||||||
|
ISNULL((SELECT Cnt FROM AllSubtreeCounts WHERE LegSide = 1), 0) AS AllRight,
|
||||||
|
-- Active club only (new سازمان چپ/راست / کل)
|
||||||
|
ISNULL((SELECT Cnt FROM ActiveClubCounts WHERE LegSide = 0), 0) AS ActiveLeft,
|
||||||
|
ISNULL((SELECT Cnt FROM ActiveClubCounts WHERE LegSide = 1), 0) AS ActiveRight,
|
||||||
|
ISNULL((SELECT Cnt FROM ActiveClubCounts WHERE LegSide = 0), 0)
|
||||||
|
+ ISNULL((SELECT Cnt FROM ActiveClubCounts WHERE LegSide = 1), 0) AS ActiveTotal,
|
||||||
|
-- Gold / Silver package holders (active + current cycle)
|
||||||
|
ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 0 AND PackageId = @GoldPackageId), 0) AS GoldLeft,
|
||||||
|
ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 1 AND PackageId = @GoldPackageId), 0) AS GoldRight,
|
||||||
|
ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 0 AND PackageId = @SilverPackageId), 0) AS SilverLeft,
|
||||||
|
ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 1 AND PackageId = @SilverPackageId), 0) AS SilverRight,
|
||||||
|
-- Gaps: Active − (Gold+Silver) per leg
|
||||||
|
ISNULL((SELECT Cnt FROM ActiveClubCounts WHERE LegSide = 0), 0)
|
||||||
|
- ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 0 AND PackageId = @GoldPackageId), 0)
|
||||||
|
- ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 0 AND PackageId = @SilverPackageId), 0) AS GapLeft_ActiveMinusPackages,
|
||||||
|
ISNULL((SELECT Cnt FROM ActiveClubCounts WHERE LegSide = 1), 0)
|
||||||
|
- ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 1 AND PackageId = @GoldPackageId), 0)
|
||||||
|
- ISNULL((SELECT HolderCount FROM ActivePackageHolders WHERE LegSide = 1 AND PackageId = @SilverPackageId), 0) AS GapRight_ActiveMinusPackages
|
||||||
|
OPTION (MAXRECURSION 0);
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
Rollback for Backfill_UserPackagePurchases_FromClubMemberships.sql
|
||||||
|
|
||||||
|
What it undoes:
|
||||||
|
A) 77 synthetic rows: Tx with RefId LIKE 'UPP-BACKFILL-%' + their UPP
|
||||||
|
B) 24 ManualPayment-linked UPP rows inserted by backfill
|
||||||
|
(same TransactionId as Approved ManualPayments — NOT deleting those Transactions)
|
||||||
|
|
||||||
|
BEFORE RUN:
|
||||||
|
Set @BackfillStartedAt to a timestamp slightly BEFORE you ran the backfill
|
||||||
|
(e.g. if you ran at 06:40, use '2026-08-14 06:00:00').
|
||||||
|
Part B only deletes UPP with Created >= that time so later real CreateManualPayment rows stay.
|
||||||
|
|
||||||
|
Safe to preview: leave @Commit = 0 (default) to only PRINT counts inside a rolled-back tran.
|
||||||
|
Set @Commit = 1 to actually apply.
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
DECLARE @BackfillStartedAt DATETIME2 = '2026-08-14 00:00:00'; -- << adjust
|
||||||
|
DECLARE @Commit BIT = 0; -- 0 = dry-run (rollback), 1 = commit deletes
|
||||||
|
|
||||||
|
BEGIN TRAN;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- A) Synthetic Tx + UPP (RefId marker)
|
||||||
|
------------------------------------------------------------
|
||||||
|
DECLARE @SyntheticUpp TABLE (Id BIGINT PRIMARY KEY, TransactionId BIGINT);
|
||||||
|
|
||||||
|
INSERT INTO @SyntheticUpp (Id, TransactionId)
|
||||||
|
SELECT upp.Id, upp.TransactionId
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = upp.TransactionId
|
||||||
|
WHERE t.RefId LIKE N'UPP-BACKFILL-%';
|
||||||
|
|
||||||
|
DECLARE @SynthUppCount INT = (SELECT COUNT(*) FROM @SyntheticUpp);
|
||||||
|
DECLARE @SynthTxCount INT =
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM CMS.Transactions t
|
||||||
|
WHERE t.RefId LIKE N'UPP-BACKFILL-%'
|
||||||
|
);
|
||||||
|
|
||||||
|
DELETE upp
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
INNER JOIN @SyntheticUpp s ON s.Id = upp.Id;
|
||||||
|
|
||||||
|
DELETE t
|
||||||
|
FROM CMS.Transactions t
|
||||||
|
WHERE t.RefId LIKE N'UPP-BACKFILL-%';
|
||||||
|
|
||||||
|
PRINT CONCAT('Deleted synthetic UPP: ', @SynthUppCount);
|
||||||
|
PRINT CONCAT('Deleted synthetic Transactions: ', @SynthTxCount);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- B) UPP created from ManualPayments reuse (no new Tx)
|
||||||
|
------------------------------------------------------------
|
||||||
|
DECLARE @ManualUpp TABLE (Id BIGINT PRIMARY KEY);
|
||||||
|
|
||||||
|
INSERT INTO @ManualUpp (Id)
|
||||||
|
SELECT upp.Id
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
INNER JOIN CMS.ManualPayments mp
|
||||||
|
ON mp.TransactionId = upp.TransactionId
|
||||||
|
AND mp.IsDeleted = 0
|
||||||
|
AND mp.Status = 1 -- Approved
|
||||||
|
WHERE upp.IsDeleted = 0
|
||||||
|
AND upp.PurchaseMethod = 3 -- Manual
|
||||||
|
AND upp.Created >= @BackfillStartedAt
|
||||||
|
-- exclude anything already covered as synthetic (shouldn't overlap)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM CMS.Transactions t
|
||||||
|
WHERE t.Id = upp.TransactionId AND t.RefId LIKE N'UPP-BACKFILL-%'
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE @ManualUppCount INT = (SELECT COUNT(*) FROM @ManualUpp);
|
||||||
|
|
||||||
|
DELETE upp
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
INNER JOIN @ManualUpp m ON m.Id = upp.Id;
|
||||||
|
|
||||||
|
PRINT CONCAT('Deleted ManualPayment-linked UPP (Created >= @BackfillStartedAt): ', @ManualUppCount);
|
||||||
|
PRINT CONCAT('@BackfillStartedAt used: ', CONVERT(varchar(30), @BackfillStartedAt, 126));
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Optional sanity after rollback
|
||||||
|
------------------------------------------------------------
|
||||||
|
DECLARE @Orphans INT =
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM CMS.ClubMemberships m
|
||||||
|
WHERE m.IsDeleted = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = upp.TransactionId AND t.IsDeleted = 0
|
||||||
|
WHERE upp.UserId = m.UserId
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
AND (upp.TransactionId IS NULL OR t.PaymentStatus = 0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
PRINT CONCAT('Club members without successful UPP after rollback: ', @Orphans);
|
||||||
|
PRINT '(Expect around 101 if dump-era state, if no new purchases since.)';
|
||||||
|
|
||||||
|
IF @Commit = 1
|
||||||
|
BEGIN
|
||||||
|
COMMIT;
|
||||||
|
PRINT 'Rollback COMMITTED.';
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
ROLLBACK;
|
||||||
|
PRINT 'Dry-run only — ROLLBACK. Set @Commit = 1 after verifying counts.';
|
||||||
|
END
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
Update DayaLoanContracts.ContractNumber from MerchantCreditList.
|
||||||
|
|
||||||
|
Match: NationalCode (unique cases).
|
||||||
|
Special case — same NationalCode 2490371436 on TWO users (two mobiles):
|
||||||
|
UserId=50 (09207316560, Created 2025-11-11) → C4_D8815963 (فعالسازی 1404/8/24 = 2025-11-15)
|
||||||
|
UserId=51 (09366426797, Created 2025-11-12) → C4_G8945289 (فعالسازی 1404/8/30 = 2025-11-21)
|
||||||
|
User 51 may have no DayaLoanContracts row yet → INSERT.
|
||||||
|
|
||||||
|
Script defaults to ROLLBACK; uncomment COMMIT after verifying.
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
BEGIN TRAN;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 1) Unique NationalCodes → UPDATE ContractNumber
|
||||||
|
------------------------------------------------------------
|
||||||
|
;WITH Src AS (
|
||||||
|
SELECT *
|
||||||
|
FROM (VALUES
|
||||||
|
(N'C4_C11231802', N'0312980973'),
|
||||||
|
(N'C4_H10706299', N'4060667961'),
|
||||||
|
(N'C4_E10490523', N'4072306754'),
|
||||||
|
(N'C4_C10337736', N'0054140161'),
|
||||||
|
(N'C4_Q10727431', N'5279791253'),
|
||||||
|
(N'C4_W10568862', N'1960144804'),
|
||||||
|
(N'C4_V10338780', N'4072240567'),
|
||||||
|
(N'C4_G10538762', N'6639906902'),
|
||||||
|
(N'C4_N10921338', N'0943060761'),
|
||||||
|
(N'C4_P9320383', N'2572729636'),
|
||||||
|
(N'C4_Y9276146', N'0012011061'),
|
||||||
|
(N'C4_Q9465813', N'2480017796'),
|
||||||
|
(N'C4_A9107753', N'0450314103'),
|
||||||
|
(N'C4_B9348378', N'0452314488'),
|
||||||
|
(N'C4_U9595222', N'2062876815'),
|
||||||
|
(N'C4_X9630520', N'0680164286'),
|
||||||
|
(N'C4_J9199624', N'0681699401'),
|
||||||
|
(N'C4_B9813553', N'0055680364'),
|
||||||
|
(N'C4_M9994735', N'0793851572'),
|
||||||
|
(N'C4_S9301452', N'0082242925'),
|
||||||
|
(N'C4_V9257168', N'4459676451'),
|
||||||
|
(N'C4_T9961063', N'0312342993'),
|
||||||
|
(N'C4_L9524515', N'0024757489'),
|
||||||
|
(N'C4_Q9947535', N'2480259668'),
|
||||||
|
(N'C4_T9892089', N'2451477016'),
|
||||||
|
(N'C4_N9945163', N'1289603669'),
|
||||||
|
(N'C4_W8785358', N'0322922720'),
|
||||||
|
(N'C4_J8388494', N'2002637903'),
|
||||||
|
(N'C4_V8165679', N'0081093829'),
|
||||||
|
(N'C4_T8579002', N'5289780335')
|
||||||
|
-- 2490371436 handled separately by UserId below
|
||||||
|
) AS v(ContractNumber, NationalCode)
|
||||||
|
)
|
||||||
|
UPDATE d
|
||||||
|
SET
|
||||||
|
d.ContractNumber = s.ContractNumber,
|
||||||
|
d.LastModified = SYSUTCDATETIME()
|
||||||
|
FROM CMS.DayaLoanContracts d
|
||||||
|
INNER JOIN Src s ON s.NationalCode = d.NationalCode
|
||||||
|
WHERE d.IsDeleted = 0;
|
||||||
|
|
||||||
|
PRINT CONCAT('Updated by NationalCode (unique): ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 2) Shared NC 2490371436 — match by UserId + Excel activation date
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- UserId 50 ← earlier activation 1404/8/24 → C4_D8815963
|
||||||
|
UPDATE CMS.DayaLoanContracts
|
||||||
|
SET
|
||||||
|
ContractNumber = N'C4_D8815963',
|
||||||
|
LastModified = SYSUTCDATETIME()
|
||||||
|
WHERE IsDeleted = 0
|
||||||
|
AND UserId = 50
|
||||||
|
AND NationalCode = N'2490371436';
|
||||||
|
|
||||||
|
PRINT CONCAT('Updated UserId=50 (C4_D8815963): ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
-- UserId 51 ← later activation 1404/8/30 → C4_G8945289
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM CMS.DayaLoanContracts
|
||||||
|
WHERE IsDeleted = 0 AND UserId = 51 AND NationalCode = N'2490371436'
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
UPDATE CMS.DayaLoanContracts
|
||||||
|
SET
|
||||||
|
ContractNumber = N'C4_G8945289',
|
||||||
|
LastModified = SYSUTCDATETIME()
|
||||||
|
WHERE IsDeleted = 0
|
||||||
|
AND UserId = 51
|
||||||
|
AND NationalCode = N'2490371436';
|
||||||
|
|
||||||
|
PRINT CONCAT('Updated UserId=51 (C4_G8945289): ', @@ROWCOUNT);
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.DayaLoanContracts
|
||||||
|
(
|
||||||
|
UserId, NationalCode, ContractNumber, Status, IsProcessed,
|
||||||
|
LastCheckDate, ProcessedDate, TransactionId,
|
||||||
|
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
51,
|
||||||
|
N'2490371436',
|
||||||
|
N'C4_G8945289',
|
||||||
|
0, -- NotRequested (same as sibling row for UserId=50 in dump; adjust if needed)
|
||||||
|
0,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
SYSUTCDATETIME(),
|
||||||
|
NULL,
|
||||||
|
SYSUTCDATETIME(),
|
||||||
|
NULL,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
PRINT 'Inserted DayaLoanContracts for UserId=51 (C4_G8945289).';
|
||||||
|
END
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Verify
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
d.Id,
|
||||||
|
d.UserId,
|
||||||
|
u.Mobile,
|
||||||
|
d.NationalCode,
|
||||||
|
d.ContractNumber,
|
||||||
|
d.Status,
|
||||||
|
d.IsProcessed
|
||||||
|
FROM CMS.DayaLoanContracts d
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = d.UserId
|
||||||
|
WHERE d.IsDeleted = 0
|
||||||
|
AND d.NationalCode = N'2490371436'
|
||||||
|
ORDER BY d.UserId;
|
||||||
|
|
||||||
|
-- Uncomment to apply:
|
||||||
|
-- COMMIT;
|
||||||
|
ROLLBACK;
|
||||||
|
PRINT 'Rolled back (safety). Uncomment COMMIT and remove ROLLBACK to apply.';
|
||||||
@@ -0,0 +1,540 @@
|
|||||||
|
/*
|
||||||
|
Staging fix: complete post-Daya processing for Excel rows with status «فعال شده»
|
||||||
|
that are NOT_PROCESSED or PARTIAL.
|
||||||
|
|
||||||
|
Classification:
|
||||||
|
DONE — credit + processed + Daya UPP
|
||||||
|
PARTIAL — HasReceivedDayaCredit OR IsProcessed OR existing «اعتبار دایا» Tx in wallet
|
||||||
|
NOT_PROCESSED — none of the above (will create Tx + ledger WH)
|
||||||
|
|
||||||
|
If ExistingDayaTxId found (wallet RefrenceId → Tx Description LIKE '%اعتبار دایا%'
|
||||||
|
OR Tx.RefId = ContractNumber): REUSE that Tx — do NOT create Transaction.
|
||||||
|
|
||||||
|
NO wallet Balance/DiscountBalance charge — UserWalletHistory is ledger-only when needed.
|
||||||
|
|
||||||
|
Special UserId mapping (shared NationalCode):
|
||||||
|
C4_D8815963 / 2490371436 → UserId 50
|
||||||
|
C4_G8945289 / 2490371436 → UserId 51
|
||||||
|
|
||||||
|
@Commit = 0 → dry-run (ROLLBACK)
|
||||||
|
@Commit = 1 → apply
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
DECLARE @Commit BIT = 0; -- << set 1 to apply
|
||||||
|
|
||||||
|
DECLARE @Now DATETIME2 = SYSUTCDATETIME();
|
||||||
|
DECLARE @PackageId BIGINT;
|
||||||
|
DECLARE @PackagePrice BIGINT;
|
||||||
|
DECLARE @DiscountMultiplier DECIMAL(18,4);
|
||||||
|
DECLARE @DiscountAmount BIGINT;
|
||||||
|
|
||||||
|
SELECT TOP (1)
|
||||||
|
@PackageId = Id,
|
||||||
|
@PackagePrice = Price,
|
||||||
|
@DiscountMultiplier = DiscountMultiplier
|
||||||
|
FROM CMS.Packages
|
||||||
|
WHERE IsDeleted = 0 AND IsBasePackage = 1
|
||||||
|
ORDER BY Id;
|
||||||
|
|
||||||
|
IF @PackageId IS NULL
|
||||||
|
THROW 50001, 'Base package not found.', 1;
|
||||||
|
|
||||||
|
SET @DiscountAmount = CAST(@PackagePrice * @DiscountMultiplier AS BIGINT);
|
||||||
|
|
||||||
|
PRINT CONCAT('PackageId=', @PackageId, ' Price=', @PackagePrice, ' Discount=', @DiscountAmount);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Targets: Excel «فعال شده»
|
||||||
|
------------------------------------------------------------
|
||||||
|
DECLARE @Targets TABLE (
|
||||||
|
ContractNumber NVARCHAR(50) NOT NULL,
|
||||||
|
NationalCode NVARCHAR(20) NOT NULL,
|
||||||
|
UserIdHint BIGINT NULL -- explicit override when NC is shared
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO @Targets (ContractNumber, NationalCode, UserIdHint) VALUES
|
||||||
|
(N'C4_H10706299', N'4060667961', NULL),
|
||||||
|
(N'C4_P9320383', N'2572729636', NULL),
|
||||||
|
(N'C4_Q9465813', N'2480017796', NULL),
|
||||||
|
(N'C4_B9348378', N'0452314488', NULL),
|
||||||
|
(N'C4_X9630520', N'0680164286', NULL),
|
||||||
|
(N'C4_B9813553', N'0055680364', NULL), -- may match DayaLoanContracts.NationalCode if Users.NC differs
|
||||||
|
(N'C4_M9994735', N'0793851572', NULL),
|
||||||
|
(N'C4_S9301452', N'0082242925', NULL),
|
||||||
|
(N'C4_T9961063', N'0312342993', NULL),
|
||||||
|
(N'C4_L9524515', N'0024757489', NULL),
|
||||||
|
(N'C4_Q9947535', N'2480259668', NULL),
|
||||||
|
(N'C4_T9892089', N'2451477016', NULL),
|
||||||
|
(N'C4_W8785358', N'0322922720', NULL),
|
||||||
|
(N'C4_D8815963', N'2490371436', 50), -- earlier activation → user 50
|
||||||
|
(N'C4_G8945289', N'2490371436', 51), -- later activation → user 51
|
||||||
|
(N'C4_J8388494', N'2002637903', NULL);
|
||||||
|
|
||||||
|
DECLARE @Work TABLE (
|
||||||
|
ContractNumber NVARCHAR(50),
|
||||||
|
NationalCode NVARCHAR(20),
|
||||||
|
UserId BIGINT,
|
||||||
|
DayaContractId BIGINT NULL,
|
||||||
|
HasCredit BIT,
|
||||||
|
IsProcessed BIT,
|
||||||
|
HasDayaUpp BIT,
|
||||||
|
ExistingDayaTxId BIGINT NULL, -- Tx از قبل با شرح «اعتبار دایا» / RefId قرارداد (شارژ شده)
|
||||||
|
Verdict NVARCHAR(20)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO @Work (ContractNumber, NationalCode, UserId, DayaContractId, HasCredit, IsProcessed, HasDayaUpp, ExistingDayaTxId, Verdict)
|
||||||
|
SELECT
|
||||||
|
t.ContractNumber,
|
||||||
|
t.NationalCode,
|
||||||
|
u.Id,
|
||||||
|
d.Id,
|
||||||
|
CAST(ISNULL(u.HasReceivedDayaCredit, 0) AS BIT),
|
||||||
|
CAST(ISNULL(d.IsProcessed, 0) AS BIT),
|
||||||
|
CAST(CASE WHEN EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
LEFT JOIN CMS.Transactions tx ON tx.Id = upp.TransactionId AND tx.IsDeleted = 0
|
||||||
|
WHERE upp.UserId = u.Id
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
AND upp.PurchaseMethod = 1
|
||||||
|
AND (upp.TransactionId IS NULL OR tx.PaymentStatus = 0)
|
||||||
|
) THEN 1 ELSE 0 END AS BIT),
|
||||||
|
etx.TxId,
|
||||||
|
CASE
|
||||||
|
WHEN u.Id IS NULL THEN N'NO_USER'
|
||||||
|
WHEN ISNULL(u.HasReceivedDayaCredit, 0) = 1
|
||||||
|
AND ISNULL(d.IsProcessed, 0) = 1
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
LEFT JOIN CMS.Transactions tx ON tx.Id = upp.TransactionId AND tx.IsDeleted = 0
|
||||||
|
WHERE upp.UserId = u.Id
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
AND upp.PurchaseMethod = 1
|
||||||
|
AND (upp.TransactionId IS NULL OR tx.PaymentStatus = 0)
|
||||||
|
) THEN N'DONE'
|
||||||
|
-- قبلاً شارژ دایا در کیف/تراکنش هست ولی فلگ/UPP/قرارداد کامل نیست
|
||||||
|
WHEN etx.TxId IS NOT NULL THEN N'PARTIAL'
|
||||||
|
WHEN ISNULL(u.HasReceivedDayaCredit, 0) = 1
|
||||||
|
OR ISNULL(d.IsProcessed, 0) = 1 THEN N'PARTIAL'
|
||||||
|
ELSE N'NOT_PROCESSED'
|
||||||
|
END
|
||||||
|
FROM @Targets t
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1) u0.*
|
||||||
|
FROM CMS.Users u0
|
||||||
|
WHERE u0.IsDeleted = 0
|
||||||
|
AND (
|
||||||
|
(t.UserIdHint IS NOT NULL AND u0.Id = t.UserIdHint)
|
||||||
|
OR (t.UserIdHint IS NULL AND u0.NationalCode = t.NationalCode)
|
||||||
|
OR (t.UserIdHint IS NULL AND EXISTS (
|
||||||
|
SELECT 1 FROM CMS.DayaLoanContracts dx
|
||||||
|
WHERE dx.UserId = u0.Id
|
||||||
|
AND dx.NationalCode = t.NationalCode
|
||||||
|
AND dx.IsDeleted = 0
|
||||||
|
))
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN t.UserIdHint IS NOT NULL AND u0.Id = t.UserIdHint THEN 0 ELSE 1 END,
|
||||||
|
CASE WHEN u0.NationalCode = t.NationalCode THEN 0 ELSE 1 END,
|
||||||
|
u0.Id
|
||||||
|
) u
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1) d0.*
|
||||||
|
FROM CMS.DayaLoanContracts d0
|
||||||
|
WHERE d0.UserId = u.Id
|
||||||
|
AND (
|
||||||
|
d0.ContractNumber = t.ContractNumber
|
||||||
|
OR d0.NationalCode = t.NationalCode
|
||||||
|
OR d0.IsDeleted IN (0, 1)
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN d0.ContractNumber = t.ContractNumber THEN 0 ELSE 1 END,
|
||||||
|
CASE WHEN d0.IsDeleted = 0 THEN 0 ELSE 1 END,
|
||||||
|
d0.Id DESC
|
||||||
|
) d
|
||||||
|
OUTER APPLY (
|
||||||
|
-- تراکنش دایا که از قبل شارژ شده (مرجع کیف پول یا RefId قرارداد)
|
||||||
|
SELECT TOP (1) t0.Id AS TxId
|
||||||
|
FROM CMS.Transactions t0
|
||||||
|
WHERE t0.IsDeleted = 0
|
||||||
|
AND t0.PaymentStatus = 0
|
||||||
|
AND (
|
||||||
|
t0.RefId = t.ContractNumber
|
||||||
|
OR (
|
||||||
|
t0.Description LIKE N'%اعتبار دایا%'
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserWallets w
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id
|
||||||
|
WHERE w.UserId = u.Id
|
||||||
|
AND h.RefrenceId = t0.Id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN t0.RefId = t.ContractNumber THEN 0 ELSE 1 END,
|
||||||
|
t0.Id DESC
|
||||||
|
) etx;
|
||||||
|
|
||||||
|
PRINT '=== Before ===';
|
||||||
|
SELECT Verdict, COUNT(*) AS Cnt FROM @Work GROUP BY Verdict;
|
||||||
|
SELECT ContractNumber, NationalCode, UserId, DayaContractId, HasCredit, IsProcessed, HasDayaUpp,
|
||||||
|
ExistingDayaTxId, Verdict
|
||||||
|
FROM @Work
|
||||||
|
ORDER BY Verdict, ContractNumber;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- PREVIEW: ledger actions (no wallet charge)
|
||||||
|
------------------------------------------------------------
|
||||||
|
PRINT '=== LEDGER PREVIEW (Tx + WH history-only — NO wallet charge) ===';
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
w.ContractNumber,
|
||||||
|
w.NationalCode,
|
||||||
|
w.UserId,
|
||||||
|
w.Verdict,
|
||||||
|
w.HasCredit,
|
||||||
|
w.ExistingDayaTxId,
|
||||||
|
CASE
|
||||||
|
WHEN w.UserId IS NULL THEN N'SKIP_NO_USER'
|
||||||
|
WHEN w.ExistingDayaTxId IS NOT NULL THEN N'REUSE_TX_ENSURE_WH_UPP'
|
||||||
|
WHEN w.HasCredit = 0 THEN N'CREATE_TX_WH_UPP'
|
||||||
|
ELSE N'SET_FLAGS_ONLY_UPP'
|
||||||
|
END AS LedgerAction
|
||||||
|
FROM @Work w
|
||||||
|
WHERE w.Verdict IN (N'NOT_PROCESSED', N'PARTIAL')
|
||||||
|
ORDER BY LedgerAction, w.ContractNumber;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN w.UserId IS NULL THEN N'SKIP_NO_USER'
|
||||||
|
WHEN w.ExistingDayaTxId IS NOT NULL THEN N'REUSE_TX_ENSURE_WH_UPP'
|
||||||
|
WHEN w.HasCredit = 0 THEN N'CREATE_TX_WH_UPP'
|
||||||
|
ELSE N'SET_FLAGS_ONLY_UPP'
|
||||||
|
END AS LedgerAction,
|
||||||
|
COUNT(*) AS Cnt
|
||||||
|
FROM @Work w
|
||||||
|
WHERE w.Verdict IN (N'NOT_PROCESSED', N'PARTIAL')
|
||||||
|
GROUP BY
|
||||||
|
CASE
|
||||||
|
WHEN w.UserId IS NULL THEN N'SKIP_NO_USER'
|
||||||
|
WHEN w.ExistingDayaTxId IS NOT NULL THEN N'REUSE_TX_ENSURE_WH_UPP'
|
||||||
|
WHEN w.HasCredit = 0 THEN N'CREATE_TX_WH_UPP'
|
||||||
|
ELSE N'SET_FLAGS_ONLY_UPP'
|
||||||
|
END
|
||||||
|
ORDER BY LedgerAction;
|
||||||
|
|
||||||
|
DECLARE @PreviewCreateTxCount INT = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM @Work w
|
||||||
|
WHERE w.Verdict IN (N'NOT_PROCESSED', N'PARTIAL')
|
||||||
|
AND w.UserId IS NOT NULL
|
||||||
|
AND w.ExistingDayaTxId IS NULL
|
||||||
|
AND w.HasCredit = 0
|
||||||
|
);
|
||||||
|
PRINT CONCAT('>>> CREATE_TX (no wallet charge) count = ', @PreviewCreateTxCount);
|
||||||
|
|
||||||
|
BEGIN TRAN;
|
||||||
|
|
||||||
|
DECLARE @ContractNumber NVARCHAR(50), @NationalCode NVARCHAR(20), @UserId BIGINT;
|
||||||
|
DECLARE @DayaContractId BIGINT, @HasCredit BIT, @IsProcessed BIT, @HasDayaUpp BIT, @Verdict NVARCHAR(20);
|
||||||
|
DECLARE @ExistingDayaTxId BIGINT;
|
||||||
|
DECLARE @TxId BIGINT, @WalletId BIGINT, @NetworkBalance BIGINT, @Action NVARCHAR(200);
|
||||||
|
|
||||||
|
DECLARE @CreateTxCount INT = 0;
|
||||||
|
DECLARE @ReuseCount INT = 0;
|
||||||
|
DECLARE @SkipFlagsOnlyCount INT = 0;
|
||||||
|
DECLARE @WhCreatedCount INT = 0;
|
||||||
|
|
||||||
|
DECLARE @Log TABLE (
|
||||||
|
ContractNumber NVARCHAR(50),
|
||||||
|
UserId BIGINT,
|
||||||
|
VerdictBefore NVARCHAR(20),
|
||||||
|
ActionTaken NVARCHAR(200),
|
||||||
|
TxId BIGINT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
|
||||||
|
SELECT ContractNumber, NationalCode, UserId, DayaContractId, HasCredit, IsProcessed, HasDayaUpp, ExistingDayaTxId, Verdict
|
||||||
|
FROM @Work
|
||||||
|
WHERE Verdict IN (N'NOT_PROCESSED', N'PARTIAL');
|
||||||
|
|
||||||
|
OPEN cur;
|
||||||
|
FETCH NEXT FROM cur INTO @ContractNumber, @NationalCode, @UserId, @DayaContractId, @HasCredit, @IsProcessed, @HasDayaUpp, @ExistingDayaTxId, @Verdict;
|
||||||
|
|
||||||
|
WHILE @@FETCH_STATUS = 0
|
||||||
|
BEGIN
|
||||||
|
SET @TxId = NULL;
|
||||||
|
SET @Action = N'';
|
||||||
|
|
||||||
|
IF @UserId IS NULL
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO @Log VALUES (@ContractNumber, NULL, @Verdict, N'SKIP NO_USER', NULL);
|
||||||
|
GOTO NextRow;
|
||||||
|
END
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Ensure / revive DayaLoanContract
|
||||||
|
------------------------------------------------------------
|
||||||
|
IF @DayaContractId IS NULL
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.DayaLoanContracts
|
||||||
|
(
|
||||||
|
UserId, NationalCode, ContractNumber, Status, IsProcessed,
|
||||||
|
LastCheckDate, ProcessedDate, TransactionId,
|
||||||
|
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@UserId, @NationalCode, @ContractNumber, 2, 0,
|
||||||
|
@Now, NULL, NULL,
|
||||||
|
@Now, NULL, @Now, NULL, 0
|
||||||
|
);
|
||||||
|
SET @DayaContractId = SCOPE_IDENTITY();
|
||||||
|
SET @Action = @Action + N'INSERT_CONTRACT;';
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
UPDATE CMS.DayaLoanContracts
|
||||||
|
SET
|
||||||
|
IsDeleted = 0,
|
||||||
|
NationalCode = @NationalCode,
|
||||||
|
ContractNumber = @ContractNumber,
|
||||||
|
Status = 2, -- Received («فعال شده»)
|
||||||
|
LastCheckDate = @Now,
|
||||||
|
LastModified = @Now
|
||||||
|
WHERE Id = @DayaContractId;
|
||||||
|
SET @Action = @Action + N'UPDATE_CONTRACT;';
|
||||||
|
END
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Resolve Tx (reuse existing or create ledger Tx — NO wallet charge)
|
||||||
|
------------------------------------------------------------
|
||||||
|
IF @ExistingDayaTxId IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
SET @TxId = @ExistingDayaTxId;
|
||||||
|
SET @Action = @Action + CONCAT(N'REUSE_EXISTING_DAYA_TX=', @TxId, N';');
|
||||||
|
SET @ReuseCount = @ReuseCount + 1;
|
||||||
|
|
||||||
|
UPDATE CMS.Users
|
||||||
|
SET
|
||||||
|
PackagePurchaseMethod = 1,
|
||||||
|
HasReceivedDayaCredit = 1,
|
||||||
|
DayaCreditReceivedAt = COALESCE(DayaCreditReceivedAt, @Now),
|
||||||
|
LastModified = @Now
|
||||||
|
WHERE Id = @UserId;
|
||||||
|
|
||||||
|
SET @HasCredit = 1;
|
||||||
|
SET @Action = @Action + N'SET_USER_FLAGS;';
|
||||||
|
END
|
||||||
|
ELSE IF @HasCredit = 0
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.Transactions
|
||||||
|
(
|
||||||
|
Amount, Description, PaymentStatus, PaymentDate, RefId, Type,
|
||||||
|
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@PackagePrice,
|
||||||
|
CONCAT(N'دریافت اعتبار دایا - قرارداد ', @ContractNumber),
|
||||||
|
0,
|
||||||
|
@Now,
|
||||||
|
@ContractNumber,
|
||||||
|
2,
|
||||||
|
@Now, NULL, @Now, NULL, 0
|
||||||
|
);
|
||||||
|
SET @TxId = SCOPE_IDENTITY();
|
||||||
|
SET @CreateTxCount = @CreateTxCount + 1;
|
||||||
|
|
||||||
|
UPDATE CMS.Users
|
||||||
|
SET
|
||||||
|
HasReceivedDayaCredit = 1,
|
||||||
|
DayaCreditReceivedAt = @Now,
|
||||||
|
PackagePurchaseMethod = 1,
|
||||||
|
LastModified = @Now
|
||||||
|
WHERE Id = @UserId;
|
||||||
|
|
||||||
|
SET @HasCredit = 1;
|
||||||
|
SET @Action = @Action + N'CREATE_TX+USER_FLAGS;';
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
UPDATE CMS.Users
|
||||||
|
SET
|
||||||
|
PackagePurchaseMethod = 1,
|
||||||
|
HasReceivedDayaCredit = 1,
|
||||||
|
DayaCreditReceivedAt = COALESCE(DayaCreditReceivedAt, @Now),
|
||||||
|
LastModified = @Now
|
||||||
|
WHERE Id = @UserId;
|
||||||
|
SET @SkipFlagsOnlyCount = @SkipFlagsOnlyCount + 1;
|
||||||
|
SET @Action = @Action + N'SET_FLAGS_ONLY;';
|
||||||
|
END
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- UserWalletHistory for Tx (ledger-only — NO Balance/Discount charge)
|
||||||
|
------------------------------------------------------------
|
||||||
|
IF @TxId IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
SELECT @WalletId = Id, @NetworkBalance = COALESCE(NetworkBalance, 0)
|
||||||
|
FROM CMS.UserWallets
|
||||||
|
WHERE UserId = @UserId AND IsDeleted = 0;
|
||||||
|
|
||||||
|
IF @WalletId IS NULL
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.UserWallets
|
||||||
|
(UserId, Balance, NetworkBalance, DiscountBalance, WalletMode, Created, LastModified, IsDeleted)
|
||||||
|
VALUES (@UserId, 0, 0, 0, 0, @Now, @Now, 0);
|
||||||
|
SET @WalletId = SCOPE_IDENTITY();
|
||||||
|
SET @NetworkBalance = 0;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
WHERE h.WalletId = @WalletId
|
||||||
|
AND h.IsDeleted = 0
|
||||||
|
AND h.RefrenceId = @TxId
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.UserWalletHistories
|
||||||
|
(
|
||||||
|
WalletId, CurrentBalance, ChangeValue, CurrentNetworkBalance, ChangeNerworkValue,
|
||||||
|
CurrentDiscountBalance, ChangeDiscountValue, IsIncrease, RefrenceId, PackageId,
|
||||||
|
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@WalletId, 0, @PackagePrice, @NetworkBalance, 0,
|
||||||
|
0, @DiscountAmount, 1, @TxId, @PackageId,
|
||||||
|
@Now, NULL, @Now, NULL, 0
|
||||||
|
);
|
||||||
|
SET @WhCreatedCount = @WhCreatedCount + 1;
|
||||||
|
SET @Action = @Action + N'INSERT_WH_HISTORY;';
|
||||||
|
END
|
||||||
|
END
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Ensure successful Daya UPP (no duplicate if already exists)
|
||||||
|
------------------------------------------------------------
|
||||||
|
IF @HasDayaUpp = 0
|
||||||
|
BEGIN
|
||||||
|
IF @TxId IS NULL
|
||||||
|
BEGIN
|
||||||
|
-- فقط لجر؛ بدون شارژ کیف (نباید برای کسی که Tx دایا دارد به اینجا برسد)
|
||||||
|
INSERT INTO CMS.Transactions
|
||||||
|
(
|
||||||
|
Amount, Description, PaymentStatus, PaymentDate, RefId, Type,
|
||||||
|
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@PackagePrice,
|
||||||
|
CONCAT(N'لجر UPP دایا (بدون شارژ مجدد) - قرارداد ', @ContractNumber),
|
||||||
|
0,
|
||||||
|
@Now,
|
||||||
|
CONCAT(N'UPP-DAYA-FIX-', @ContractNumber),
|
||||||
|
2,
|
||||||
|
@Now, NULL, @Now, NULL, 0
|
||||||
|
);
|
||||||
|
SET @TxId = SCOPE_IDENTITY();
|
||||||
|
SET @Action = @Action + N'LEDGER_TX;';
|
||||||
|
END
|
||||||
|
|
||||||
|
INSERT INTO CMS.UserPackagePurchases
|
||||||
|
(
|
||||||
|
UserId, PackageId, PurchaseMethod, PurchasedAt, Amount,
|
||||||
|
OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@UserId, @PackageId, 1, @Now, @PackagePrice,
|
||||||
|
NULL, @TxId, @Now, NULL, @Now, NULL, 0
|
||||||
|
);
|
||||||
|
SET @Action = @Action + N'INSERT_UPP;';
|
||||||
|
END
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Mark contract processed
|
||||||
|
------------------------------------------------------------
|
||||||
|
UPDATE CMS.DayaLoanContracts
|
||||||
|
SET
|
||||||
|
IsProcessed = 1,
|
||||||
|
ProcessedDate = COALESCE(ProcessedDate, @Now),
|
||||||
|
TransactionId = COALESCE(TransactionId, @TxId),
|
||||||
|
Status = 2,
|
||||||
|
ContractNumber = @ContractNumber,
|
||||||
|
IsDeleted = 0,
|
||||||
|
LastModified = @Now
|
||||||
|
WHERE Id = @DayaContractId;
|
||||||
|
|
||||||
|
SET @Action = @Action + N'MARK_PROCESSED;';
|
||||||
|
|
||||||
|
INSERT INTO @Log VALUES (@ContractNumber, @UserId, @Verdict, @Action, @TxId);
|
||||||
|
|
||||||
|
NextRow:
|
||||||
|
FETCH NEXT FROM cur INTO @ContractNumber, @NationalCode, @UserId, @DayaContractId, @HasCredit, @IsProcessed, @HasDayaUpp, @ExistingDayaTxId, @Verdict;
|
||||||
|
END
|
||||||
|
|
||||||
|
CLOSE cur;
|
||||||
|
DEALLOCATE cur;
|
||||||
|
|
||||||
|
PRINT '=== LEDGER SUMMARY (no wallet charge) ===';
|
||||||
|
PRINT CONCAT('CREATE_TX count = ', @CreateTxCount);
|
||||||
|
PRINT CONCAT('REUSE_TX count = ', @ReuseCount);
|
||||||
|
PRINT CONCAT('SET_FLAGS_ONLY count = ', @SkipFlagsOnlyCount);
|
||||||
|
PRINT CONCAT('WH history created = ', @WhCreatedCount);
|
||||||
|
|
||||||
|
PRINT '=== Actions ===';
|
||||||
|
SELECT * FROM @Log ORDER BY VerdictBefore, ContractNumber;
|
||||||
|
|
||||||
|
-- Re-score after changes
|
||||||
|
PRINT '=== After (same targets) ===';
|
||||||
|
SELECT
|
||||||
|
t.ContractNumber,
|
||||||
|
t.NationalCode,
|
||||||
|
u.Id AS UserId,
|
||||||
|
u.HasReceivedDayaCredit,
|
||||||
|
d.IsProcessed,
|
||||||
|
d.ContractNumber AS DbContractNumber,
|
||||||
|
d.Status,
|
||||||
|
CASE WHEN EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases upp
|
||||||
|
LEFT JOIN CMS.Transactions tx ON tx.Id = upp.TransactionId AND tx.IsDeleted = 0
|
||||||
|
WHERE upp.UserId = u.Id AND upp.IsDeleted = 0 AND upp.PurchaseMethod = 1
|
||||||
|
AND (upp.TransactionId IS NULL OR tx.PaymentStatus = 0)
|
||||||
|
) THEN 1 ELSE 0 END AS HasDayaUpp,
|
||||||
|
CASE
|
||||||
|
WHEN u.Id IS NULL THEN N'NO_USER'
|
||||||
|
WHEN u.HasReceivedDayaCredit = 1 AND d.IsProcessed = 1
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases upp
|
||||||
|
LEFT JOIN CMS.Transactions tx ON tx.Id = upp.TransactionId AND tx.IsDeleted = 0
|
||||||
|
WHERE upp.UserId = u.Id AND upp.IsDeleted = 0 AND upp.PurchaseMethod = 1
|
||||||
|
AND (upp.TransactionId IS NULL OR tx.PaymentStatus = 0)
|
||||||
|
) THEN N'DONE'
|
||||||
|
WHEN u.HasReceivedDayaCredit = 1 OR d.IsProcessed = 1 THEN N'PARTIAL'
|
||||||
|
ELSE N'NOT_PROCESSED'
|
||||||
|
END AS Verdict
|
||||||
|
FROM @Targets t
|
||||||
|
LEFT JOIN CMS.Users u ON u.IsDeleted = 0 AND (
|
||||||
|
(t.UserIdHint IS NOT NULL AND u.Id = t.UserIdHint) OR (t.UserIdHint IS NULL AND u.NationalCode = t.NationalCode)
|
||||||
|
)
|
||||||
|
LEFT JOIN CMS.DayaLoanContracts d ON d.IsDeleted = 0 AND d.UserId = u.Id
|
||||||
|
AND (d.ContractNumber = t.ContractNumber OR d.NationalCode = t.NationalCode)
|
||||||
|
ORDER BY Verdict, t.ContractNumber;
|
||||||
|
|
||||||
|
IF @Commit = 1
|
||||||
|
BEGIN
|
||||||
|
COMMIT;
|
||||||
|
PRINT 'COMMITTED.';
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
ROLLBACK;
|
||||||
|
PRINT 'Dry-run ROLLBACK. Set @Commit = 1 to apply.';
|
||||||
|
END
|
||||||
@@ -0,0 +1,547 @@
|
|||||||
|
/*
|
||||||
|
Fix users wrongly marked DayaLoan who were manually charged (RefId LIKE 'manualy%').
|
||||||
|
|
||||||
|
Scope: ONLY users linked to manualy transactions — NOT the full 47-user WrongDaya list.
|
||||||
|
Real Daya users (RefId LIKE 'C4%') are excluded automatically.
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- PurchaseMethod 1→3 on Users / Club / Cycle / UPP
|
||||||
|
- Clear HasReceivedDayaCredit / DayaCreditReceivedAt
|
||||||
|
- Soft-delete stub DayaLoanContracts (unprocessed, no Tx)
|
||||||
|
- Fix broken WH.RefrenceId where CreatedBy hint points to existing manualy Tx
|
||||||
|
- Create missing UPP Manual(3) reusing existing manualy Tx (never create new Tx)
|
||||||
|
- Create missing WH rows only when Tx exists but no WH links to it (history-only, no charge)
|
||||||
|
|
||||||
|
RefrenceId=0 rule:
|
||||||
|
Tx is resolved ONLY via numeric CreatedBy hint when that Tx exists with RefId manualy%.
|
||||||
|
Mobile-only or manualy{N} suffix alone is NOT used to assume a Tx exists.
|
||||||
|
|
||||||
|
PackagePurchaseMethod: None=0, DayaLoan=1, DirectPurchase=2, Manual=3
|
||||||
|
|
||||||
|
Defaults to ROLLBACK. Uncomment COMMIT after verifying PRINT output.
|
||||||
|
Run 10-Diagnose_Manualy_Transaction_Map.sql first.
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
BEGIN TRAN;
|
||||||
|
|
||||||
|
DECLARE @Now DATETIME2 = SYSUTCDATETIME();
|
||||||
|
|
||||||
|
DECLARE @BasePackageId BIGINT;
|
||||||
|
DECLARE @BasePackagePrice BIGINT;
|
||||||
|
|
||||||
|
SELECT TOP (1)
|
||||||
|
@BasePackageId = Id,
|
||||||
|
@BasePackagePrice = Price
|
||||||
|
FROM CMS.Packages
|
||||||
|
WHERE IsDeleted = 0 AND IsBasePackage = 1
|
||||||
|
ORDER BY Id;
|
||||||
|
|
||||||
|
IF @BasePackageId IS NULL
|
||||||
|
THROW 50001, 'Base package not found.', 1;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Scope: users linked to manualy transactions
|
||||||
|
-- (duplicate WH→Tx links exist from migration; rank by mobile match + charge amount + date)
|
||||||
|
------------------------------------------------------------
|
||||||
|
DECLARE @ManualyUsers TABLE (
|
||||||
|
UserId BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
ManualyTxId BIGINT NULL,
|
||||||
|
LinkSource NVARCHAR(50) NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT t.Id AS TxId, t.RefId, t.Amount, t.PaymentDate, t.CreatedBy, t.LastModifiedBy
|
||||||
|
INTO #ManualyTx
|
||||||
|
FROM CMS.Transactions t
|
||||||
|
WHERE t.IsDeleted = 0 AND t.RefId LIKE N'manualy%';
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
mt.TxId,
|
||||||
|
w.UserId,
|
||||||
|
h.Id AS WhId,
|
||||||
|
h.ChangeValue,
|
||||||
|
CASE
|
||||||
|
WHEN NULLIF(LTRIM(RTRIM(mt.CreatedBy)), N'') LIKE N'09%'
|
||||||
|
AND u.Mobile = NULLIF(LTRIM(RTRIM(mt.CreatedBy)), N'') THEN 1
|
||||||
|
WHEN NULLIF(LTRIM(RTRIM(mt.LastModifiedBy)), N'') LIKE N'09%'
|
||||||
|
AND u.Mobile = NULLIF(LTRIM(RTRIM(mt.LastModifiedBy)), N'') THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS MobileMatch,
|
||||||
|
CASE WHEN h.ChangeValue >= mt.Amount THEN 1 ELSE 0 END AS FullCharge,
|
||||||
|
ABS(DATEDIFF(SECOND, h.Created, mt.PaymentDate)) AS TimeDeltaSec
|
||||||
|
INTO #WhLinks
|
||||||
|
FROM #ManualyTx mt
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.RefrenceId = mt.TxId AND h.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = w.UserId AND u.IsDeleted = 0;
|
||||||
|
|
||||||
|
SELECT UserId, TxId, WhId, MobileMatch, FullCharge,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY TxId
|
||||||
|
ORDER BY MobileMatch DESC, FullCharge DESC, TimeDeltaSec ASC, WhId ASC
|
||||||
|
) AS rn
|
||||||
|
INTO #BestWhPerTx
|
||||||
|
FROM #WhLinks;
|
||||||
|
|
||||||
|
INSERT INTO @ManualyUsers (UserId, ManualyTxId, LinkSource)
|
||||||
|
SELECT b.UserId, b.TxId, N'WH_REF_BEST'
|
||||||
|
FROM #BestWhPerTx b
|
||||||
|
WHERE b.rn = 1
|
||||||
|
AND (b.MobileMatch = 1 OR b.FullCharge = 1);
|
||||||
|
|
||||||
|
INSERT INTO @ManualyUsers (UserId, ManualyTxId, LinkSource)
|
||||||
|
SELECT DISTINCT w.UserId, t.Id, N'WH_CREATEDBY_HINT'
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
|
||||||
|
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = w.UserId AND u.IsDeleted = 0
|
||||||
|
WHERE h.IsDeleted = 0
|
||||||
|
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
|
||||||
|
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0
|
||||||
|
AND (
|
||||||
|
NULLIF(LTRIM(RTRIM(t.CreatedBy)), N'') NOT LIKE N'09%'
|
||||||
|
OR u.Mobile IN (
|
||||||
|
NULLIF(LTRIM(RTRIM(t.CreatedBy)), N''),
|
||||||
|
NULLIF(LTRIM(RTRIM(t.LastModifiedBy)), N'')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM @ManualyUsers m WHERE m.UserId = w.UserId);
|
||||||
|
|
||||||
|
INSERT INTO @ManualyUsers (UserId, ManualyTxId, LinkSource)
|
||||||
|
SELECT DISTINCT u.Id, t.Id, N'TX_MOBILE'
|
||||||
|
FROM CMS.Transactions t
|
||||||
|
INNER JOIN CMS.Users u ON u.IsDeleted = 0
|
||||||
|
AND u.Mobile IN (
|
||||||
|
NULLIF(LTRIM(RTRIM(t.CreatedBy)), N''),
|
||||||
|
NULLIF(LTRIM(RTRIM(t.LastModifiedBy)), N'')
|
||||||
|
)
|
||||||
|
WHERE t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM @ManualyUsers m WHERE m.UserId = u.Id);
|
||||||
|
|
||||||
|
-- Txs without mobile on row: keep best full-charge WH only (early batch 23-31)
|
||||||
|
INSERT INTO @ManualyUsers (UserId, ManualyTxId, LinkSource)
|
||||||
|
SELECT b.UserId, b.TxId, N'WH_REF_NO_MOBILE'
|
||||||
|
FROM #BestWhPerTx b
|
||||||
|
INNER JOIN #ManualyTx mt ON mt.TxId = b.TxId
|
||||||
|
WHERE b.rn = 1
|
||||||
|
AND b.FullCharge = 1
|
||||||
|
AND (NULLIF(LTRIM(RTRIM(mt.CreatedBy)), N'') IS NULL OR NULLIF(LTRIM(RTRIM(mt.CreatedBy)), N'') NOT LIKE N'09%')
|
||||||
|
AND (NULLIF(LTRIM(RTRIM(mt.LastModifiedBy)), N'') IS NULL OR NULLIF(LTRIM(RTRIM(mt.LastModifiedBy)), N'') NOT LIKE N'09%')
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM @ManualyUsers m WHERE m.UserId = b.UserId);
|
||||||
|
|
||||||
|
DROP TABLE #BestWhPerTx;
|
||||||
|
DROP TABLE #WhLinks;
|
||||||
|
DROP TABLE #ManualyTx;
|
||||||
|
|
||||||
|
DECLARE @ScopeCount INT = (SELECT COUNT(*) FROM @ManualyUsers);
|
||||||
|
PRINT CONCAT('manualy scope users: ', @ScopeCount);
|
||||||
|
PRINT CONCAT('BasePackageId=', @BasePackageId, ' Price=', @BasePackagePrice);
|
||||||
|
|
||||||
|
SELECT mu.UserId, u.Mobile, u.FirstName, u.LastName,
|
||||||
|
u.PackagePurchaseMethod, mu.ManualyTxId, mu.LinkSource
|
||||||
|
FROM @ManualyUsers mu
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = mu.UserId AND u.IsDeleted = 0
|
||||||
|
ORDER BY mu.UserId;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Preview
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT 'Users with manualy Tx resolved' AS CheckName, COUNT(*) AS Cnt
|
||||||
|
FROM @ManualyUsers WHERE ManualyTxId IS NOT NULL
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Users in scope without resolved Tx', COUNT(*)
|
||||||
|
FROM @ManualyUsers WHERE ManualyTxId IS NULL
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Broken WH rows fixable via CreatedBy hint', COUNT(*)
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
INNER JOIN @ManualyUsers mu ON mu.UserId = w.UserId
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
|
||||||
|
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
|
||||||
|
WHERE h.IsDeleted = 0
|
||||||
|
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
|
||||||
|
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Missing UPP in scope', COUNT(*)
|
||||||
|
FROM @ManualyUsers mu
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = mu.UserId AND upp.IsDeleted = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 1) Fix broken WH.RefrenceId (CreatedBy hint → existing manualy Tx)
|
||||||
|
------------------------------------------------------------
|
||||||
|
UPDATE h
|
||||||
|
SET
|
||||||
|
h.RefrenceId = t.Id,
|
||||||
|
h.LastModified = @Now
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
INNER JOIN @ManualyUsers mu ON mu.UserId = w.UserId
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
|
||||||
|
AND t.IsDeleted = 0
|
||||||
|
AND t.RefId LIKE N'manualy%'
|
||||||
|
AND t.PaymentStatus = 0
|
||||||
|
WHERE h.IsDeleted = 0
|
||||||
|
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
|
||||||
|
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0;
|
||||||
|
|
||||||
|
PRINT CONCAT('WalletHistory RefrenceId fixed: ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 2) Users — DayaLoan → Manual
|
||||||
|
------------------------------------------------------------
|
||||||
|
UPDATE u
|
||||||
|
SET
|
||||||
|
u.PackagePurchaseMethod = 3,
|
||||||
|
u.HasReceivedDayaCredit = 0,
|
||||||
|
u.DayaCreditReceivedAt = NULL,
|
||||||
|
u.LastModified = @Now
|
||||||
|
FROM CMS.Users u
|
||||||
|
INNER JOIN @ManualyUsers mu ON mu.UserId = u.Id
|
||||||
|
WHERE u.IsDeleted = 0
|
||||||
|
AND (
|
||||||
|
u.PackagePurchaseMethod = 1
|
||||||
|
OR u.HasReceivedDayaCredit = 1
|
||||||
|
OR u.DayaCreditReceivedAt IS NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
PRINT CONCAT('Users updated: ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 3) ClubMemberships
|
||||||
|
------------------------------------------------------------
|
||||||
|
UPDATE m
|
||||||
|
SET m.PurchaseMethod = 3, m.LastModified = @Now
|
||||||
|
FROM CMS.ClubMemberships m
|
||||||
|
INNER JOIN @ManualyUsers mu ON mu.UserId = m.UserId
|
||||||
|
WHERE m.IsDeleted = 0 AND m.PurchaseMethod IN (0, 1);
|
||||||
|
|
||||||
|
PRINT CONCAT('ClubMemberships updated: ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 4) ClubMembershipCycles
|
||||||
|
------------------------------------------------------------
|
||||||
|
UPDATE c
|
||||||
|
SET c.PurchaseMethod = 3, c.LastModified = @Now
|
||||||
|
FROM CMS.ClubMembershipCycles c
|
||||||
|
INNER JOIN @ManualyUsers mu ON mu.UserId = c.UserId
|
||||||
|
WHERE c.IsDeleted = 0 AND c.PurchaseMethod IN (0, 1);
|
||||||
|
|
||||||
|
PRINT CONCAT('ClubMembershipCycles updated: ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 5) UserPackagePurchases (existing rows Daya → Manual)
|
||||||
|
------------------------------------------------------------
|
||||||
|
UPDATE upp
|
||||||
|
SET upp.PurchaseMethod = 3, upp.LastModified = @Now
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
INNER JOIN @ManualyUsers mu ON mu.UserId = upp.UserId
|
||||||
|
WHERE upp.IsDeleted = 0 AND upp.PurchaseMethod = 1;
|
||||||
|
|
||||||
|
PRINT CONCAT('UserPackagePurchases updated: ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 6) Soft-delete stub DayaLoanContracts
|
||||||
|
------------------------------------------------------------
|
||||||
|
UPDATE d
|
||||||
|
SET d.IsDeleted = 1, d.LastModified = @Now
|
||||||
|
FROM CMS.DayaLoanContracts d
|
||||||
|
INNER JOIN @ManualyUsers mu ON mu.UserId = d.UserId
|
||||||
|
WHERE d.IsDeleted = 0
|
||||||
|
AND d.IsProcessed = 0
|
||||||
|
AND d.TransactionId IS NULL
|
||||||
|
AND d.ProcessedDate IS NULL;
|
||||||
|
|
||||||
|
PRINT CONCAT('DayaLoanContracts soft-deleted (stubs): ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 7) Resolve best manualy Tx per user (post WH fix)
|
||||||
|
------------------------------------------------------------
|
||||||
|
;WITH TxCandidates AS (
|
||||||
|
SELECT mu.UserId, t.Id AS TxId, N'WH_REF' AS Src, 1 AS Pri
|
||||||
|
FROM @ManualyUsers mu
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.UserId = mu.UserId AND w.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = h.RefrenceId
|
||||||
|
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%' AND t.PaymentStatus = 0
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT mu.UserId, t.Id, N'WH_HINT', 2
|
||||||
|
FROM @ManualyUsers mu
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.UserId = mu.UserId AND w.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
|
||||||
|
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%' AND t.PaymentStatus = 0
|
||||||
|
WHERE (h.RefrenceId IS NULL OR h.RefrenceId = 0)
|
||||||
|
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT mu.UserId, t.Id, N'TX_MOBILE', 3
|
||||||
|
FROM @ManualyUsers mu
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = mu.UserId AND u.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.IsDeleted = 0
|
||||||
|
AND t.RefId LIKE N'manualy%' AND t.PaymentStatus = 0
|
||||||
|
AND u.Mobile IN (
|
||||||
|
NULLIF(LTRIM(RTRIM(t.CreatedBy)), N''),
|
||||||
|
NULLIF(LTRIM(RTRIM(t.LastModifiedBy)), N'')
|
||||||
|
)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT mu.UserId, mu.ManualyTxId, mu.LinkSource, 4
|
||||||
|
FROM @ManualyUsers mu
|
||||||
|
WHERE mu.ManualyTxId IS NOT NULL
|
||||||
|
),
|
||||||
|
BestTx AS (
|
||||||
|
SELECT UserId, TxId, Src,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY Pri, TxId DESC) AS rn
|
||||||
|
FROM TxCandidates
|
||||||
|
WHERE TxId IS NOT NULL
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
mu.UserId,
|
||||||
|
bt.TxId,
|
||||||
|
bt.Src AS TxSource,
|
||||||
|
COALESCE(NULLIF(cm.LastPackageId, 0), NULLIF(cm.FirstPackageId, 0), @BasePackageId) AS PackageId,
|
||||||
|
COALESCE(t.Amount, @BasePackagePrice) AS Amount,
|
||||||
|
COALESCE(t.PaymentDate, cm.ActivatedAt, @Now) AS PurchasedAt
|
||||||
|
INTO #ManualyUppPlan
|
||||||
|
FROM @ManualyUsers mu
|
||||||
|
LEFT JOIN BestTx bt ON bt.UserId = mu.UserId AND bt.rn = 1
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = bt.TxId AND t.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.ClubMemberships cm ON cm.UserId = mu.UserId AND cm.IsDeleted = 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = mu.UserId AND upp.IsDeleted = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE @UppPlanCount INT = (SELECT COUNT(*) FROM #ManualyUppPlan);
|
||||||
|
PRINT CONCAT('UPP to create: ', @UppPlanCount);
|
||||||
|
|
||||||
|
SELECT UserId, TxId, TxSource, PackageId, Amount, PurchasedAt,
|
||||||
|
CASE WHEN TxId IS NULL THEN N'SKIP_NO_MANUALY_TX' ELSE N'CREATE_UPP' END AS PlanAction
|
||||||
|
FROM #ManualyUppPlan
|
||||||
|
ORDER BY UserId;
|
||||||
|
|
||||||
|
DECLARE @UserId BIGINT;
|
||||||
|
DECLARE @TxId BIGINT;
|
||||||
|
DECLARE @PackageId BIGINT;
|
||||||
|
DECLARE @Amount BIGINT;
|
||||||
|
DECLARE @PurchasedAt DATETIME2;
|
||||||
|
DECLARE @CreatedUpp INT = 0;
|
||||||
|
DECLARE @SkippedNoTx INT = 0;
|
||||||
|
|
||||||
|
DECLARE curUpp CURSOR LOCAL FAST_FORWARD FOR
|
||||||
|
SELECT UserId, TxId, PackageId, Amount, PurchasedAt
|
||||||
|
FROM #ManualyUppPlan;
|
||||||
|
|
||||||
|
OPEN curUpp;
|
||||||
|
FETCH NEXT FROM curUpp INTO @UserId, @TxId, @PackageId, @Amount, @PurchasedAt;
|
||||||
|
|
||||||
|
WHILE @@FETCH_STATUS = 0
|
||||||
|
BEGIN
|
||||||
|
IF @TxId IS NULL OR @PackageId IS NULL OR @Amount IS NULL OR @Amount <= 0
|
||||||
|
BEGIN
|
||||||
|
SET @SkippedNoTx = @SkippedNoTx + 1;
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.UserPackagePurchases
|
||||||
|
(
|
||||||
|
UserId, PackageId, PurchaseMethod, PurchasedAt, Amount,
|
||||||
|
OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@UserId, @PackageId, 3,
|
||||||
|
@PurchasedAt, @Amount,
|
||||||
|
NULL, @TxId, @PurchasedAt, NULL, @PurchasedAt, NULL, 0
|
||||||
|
);
|
||||||
|
SET @CreatedUpp = @CreatedUpp + 1;
|
||||||
|
END
|
||||||
|
|
||||||
|
FETCH NEXT FROM curUpp INTO @UserId, @TxId, @PackageId, @Amount, @PurchasedAt;
|
||||||
|
END
|
||||||
|
|
||||||
|
CLOSE curUpp;
|
||||||
|
DEALLOCATE curUpp;
|
||||||
|
DROP TABLE #ManualyUppPlan;
|
||||||
|
|
||||||
|
PRINT CONCAT('UPP created: ', @CreatedUpp, ' | Skipped (no manualy Tx): ', @SkippedNoTx);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 8) Missing WH for resolved manualy Tx (history-only, no wallet charge)
|
||||||
|
------------------------------------------------------------
|
||||||
|
;WITH TxCandidates AS (
|
||||||
|
SELECT mu.UserId, t.Id AS TxId, 1 AS Pri
|
||||||
|
FROM @ManualyUsers mu
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.UserId = mu.UserId AND w.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = h.RefrenceId
|
||||||
|
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT mu.UserId, t.Id, 2
|
||||||
|
FROM @ManualyUsers mu
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = mu.UserId AND u.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
|
||||||
|
AND u.Mobile IN (
|
||||||
|
NULLIF(LTRIM(RTRIM(t.CreatedBy)), N''),
|
||||||
|
NULLIF(LTRIM(RTRIM(t.LastModifiedBy)), N'')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
BestTx AS (
|
||||||
|
SELECT UserId, TxId,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY UserId ORDER BY Pri, TxId DESC) AS rn
|
||||||
|
FROM TxCandidates
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
bt.UserId,
|
||||||
|
bt.TxId,
|
||||||
|
w.Id AS WalletId,
|
||||||
|
COALESCE(t.Amount, @BasePackagePrice) AS Amount,
|
||||||
|
COALESCE(t.PaymentDate, @Now) AS HistoryAt,
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(cm.LastPackageId, 0),
|
||||||
|
NULLIF(cm.FirstPackageId, 0),
|
||||||
|
@BasePackageId
|
||||||
|
) AS PackageId,
|
||||||
|
CAST(ROUND(COALESCE(t.Amount, @BasePackagePrice) * COALESCE(p.DiscountMultiplier, 2.0), 0) AS BIGINT) AS DiscountAmount,
|
||||||
|
COALESCE(w.NetworkBalance, 0) AS NetworkBalance
|
||||||
|
INTO #WhToCreate
|
||||||
|
FROM BestTx bt
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = bt.TxId AND t.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWallets w ON w.UserId = bt.UserId AND w.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.ClubMemberships cm ON cm.UserId = bt.UserId AND cm.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Packages p ON p.Id = COALESCE(
|
||||||
|
NULLIF(cm.LastPackageId, 0),
|
||||||
|
NULLIF(cm.FirstPackageId, 0),
|
||||||
|
@BasePackageId
|
||||||
|
) AND p.IsDeleted = 0
|
||||||
|
WHERE bt.rn = 1
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets wh ON wh.Id = h.WalletId AND wh.IsDeleted = 0
|
||||||
|
WHERE wh.UserId = bt.UserId
|
||||||
|
AND h.IsDeleted = 0
|
||||||
|
AND h.RefrenceId = bt.TxId
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE @WhPlanCount INT = (SELECT COUNT(*) FROM #WhToCreate);
|
||||||
|
PRINT CONCAT('WalletHistory to create (no charge): ', @WhPlanCount);
|
||||||
|
|
||||||
|
SELECT UserId, TxId, WalletId, Amount, HistoryAt, N'CREATE_HISTORY_ONLY' AS PlanAction
|
||||||
|
FROM #WhToCreate
|
||||||
|
ORDER BY UserId;
|
||||||
|
|
||||||
|
DECLARE @WalletId BIGINT;
|
||||||
|
DECLARE @DiscountAmount BIGINT;
|
||||||
|
DECLARE @HistoryAt DATETIME2;
|
||||||
|
DECLARE @NetworkBalance BIGINT;
|
||||||
|
DECLARE @CreatedWh INT = 0;
|
||||||
|
|
||||||
|
DECLARE curWh CURSOR LOCAL FAST_FORWARD FOR
|
||||||
|
SELECT UserId, TxId, WalletId, Amount, PackageId, DiscountAmount, HistoryAt, NetworkBalance
|
||||||
|
FROM #WhToCreate;
|
||||||
|
|
||||||
|
OPEN curWh;
|
||||||
|
FETCH NEXT FROM curWh INTO
|
||||||
|
@UserId, @TxId, @WalletId, @Amount, @PackageId, @DiscountAmount, @HistoryAt, @NetworkBalance;
|
||||||
|
|
||||||
|
WHILE @@FETCH_STATUS = 0
|
||||||
|
BEGIN
|
||||||
|
IF @WalletId IS NULL
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.UserWallets
|
||||||
|
(UserId, Balance, NetworkBalance, DiscountBalance, WalletMode, Created, LastModified, IsDeleted)
|
||||||
|
VALUES (@UserId, 0, 0, 0, 0, @HistoryAt, @HistoryAt, 0);
|
||||||
|
SET @WalletId = SCOPE_IDENTITY();
|
||||||
|
SET @NetworkBalance = 0;
|
||||||
|
END
|
||||||
|
|
||||||
|
INSERT INTO CMS.UserWalletHistories
|
||||||
|
(
|
||||||
|
WalletId, CurrentBalance, ChangeValue, CurrentNetworkBalance, ChangeNerworkValue,
|
||||||
|
CurrentDiscountBalance, ChangeDiscountValue, IsIncrease, RefrenceId, PackageId,
|
||||||
|
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@WalletId, 0, @Amount, @NetworkBalance, 0,
|
||||||
|
0, @DiscountAmount, 1, @TxId, @PackageId,
|
||||||
|
@HistoryAt, NULL, @HistoryAt, NULL, 0
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @CreatedWh = @CreatedWh + 1;
|
||||||
|
|
||||||
|
FETCH NEXT FROM curWh INTO
|
||||||
|
@UserId, @TxId, @WalletId, @Amount, @PackageId, @DiscountAmount, @HistoryAt, @NetworkBalance;
|
||||||
|
END
|
||||||
|
|
||||||
|
CLOSE curWh;
|
||||||
|
DEALLOCATE curWh;
|
||||||
|
DROP TABLE #WhToCreate;
|
||||||
|
|
||||||
|
PRINT CONCAT('WalletHistory created (no charge): ', @CreatedWh);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Verify
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT 'Users still Daya in scope' AS CheckName, COUNT(*) AS Cnt
|
||||||
|
FROM CMS.Users u
|
||||||
|
INNER JOIN @ManualyUsers mu ON mu.UserId = u.Id
|
||||||
|
WHERE u.IsDeleted = 0 AND u.PackagePurchaseMethod = 1
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Still missing UPP in scope', COUNT(*)
|
||||||
|
FROM @ManualyUsers mu
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = mu.UserId AND upp.IsDeleted = 0
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Broken WH Ref=0 with valid hint remaining', COUNT(*)
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
INNER JOIN @ManualyUsers mu ON mu.UserId = w.UserId
|
||||||
|
WHERE h.IsDeleted = 0
|
||||||
|
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
|
||||||
|
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM CMS.Transactions t
|
||||||
|
WHERE t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
|
||||||
|
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Active stub Daya contracts in scope', COUNT(*)
|
||||||
|
FROM CMS.DayaLoanContracts d
|
||||||
|
INNER JOIN @ManualyUsers mu ON mu.UserId = d.UserId
|
||||||
|
WHERE d.IsDeleted = 0
|
||||||
|
AND d.IsProcessed = 0
|
||||||
|
AND d.TransactionId IS NULL
|
||||||
|
AND d.ProcessedDate IS NULL;
|
||||||
|
|
||||||
|
SELECT u.Id AS UserId, u.Mobile, u.FirstName, u.LastName,
|
||||||
|
u.PackagePurchaseMethod,
|
||||||
|
(SELECT COUNT(*) FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = u.Id AND upp.IsDeleted = 0) AS UppCnt,
|
||||||
|
(SELECT COUNT(*) FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId
|
||||||
|
WHERE w.UserId = u.Id AND h.IsDeleted = 0) AS WhCnt
|
||||||
|
FROM CMS.Users u
|
||||||
|
INNER JOIN @ManualyUsers mu ON mu.UserId = u.Id
|
||||||
|
WHERE u.IsDeleted = 0
|
||||||
|
ORDER BY u.Id;
|
||||||
|
|
||||||
|
ROLLBACK;
|
||||||
|
-- COMMIT;
|
||||||
|
|
||||||
|
PRINT 'Done (rolled back by default).';
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
/*
|
||||||
|
PHASE 1 — Create UserPackagePurchases (Manual=3) only.
|
||||||
|
|
||||||
|
Scope: 17 users with verified manualy charge — both WalletHistory AND Transaction exist
|
||||||
|
and WH.RefrenceId already points to the Tx.
|
||||||
|
|
||||||
|
Hardcoded mapping (from UserWalletHistories.csv + Transactions.csv):
|
||||||
|
|
||||||
|
UserId WH Tx
|
||||||
|
------ ---- ----
|
||||||
|
75 102 23
|
||||||
|
93 103 24
|
||||||
|
96 104 25
|
||||||
|
99 105 26
|
||||||
|
47 106 28
|
||||||
|
43 107 29
|
||||||
|
45 108 30
|
||||||
|
52 122 31
|
||||||
|
116 138 42
|
||||||
|
115 139 43
|
||||||
|
119 140 44
|
||||||
|
113 141 45
|
||||||
|
103 142 46
|
||||||
|
170 292 107
|
||||||
|
171 293 108
|
||||||
|
172 294 109
|
||||||
|
175 295 110
|
||||||
|
|
||||||
|
Does NOT:
|
||||||
|
- change Users / Club / Cycle PurchaseMethod
|
||||||
|
- create or modify Transactions or WalletHistory
|
||||||
|
- re-charge wallet
|
||||||
|
|
||||||
|
PackagePurchaseMethod on UPP: Manual = 3
|
||||||
|
|
||||||
|
Defaults to ROLLBACK. Uncomment COMMIT after reviewing preview + counts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
BEGIN TRAN;
|
||||||
|
|
||||||
|
DECLARE @Now DATETIME2 = SYSUTCDATETIME();
|
||||||
|
|
||||||
|
DECLARE @BasePackageId BIGINT;
|
||||||
|
DECLARE @BasePackagePrice BIGINT;
|
||||||
|
|
||||||
|
SELECT TOP (1)
|
||||||
|
@BasePackageId = Id,
|
||||||
|
@BasePackagePrice = Price
|
||||||
|
FROM CMS.Packages
|
||||||
|
WHERE IsDeleted = 0 AND IsBasePackage = 1
|
||||||
|
ORDER BY Id;
|
||||||
|
|
||||||
|
IF @BasePackageId IS NULL
|
||||||
|
THROW 50001, 'Base package not found.', 1;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Expected mapping
|
||||||
|
------------------------------------------------------------
|
||||||
|
DECLARE @Phase1 TABLE (
|
||||||
|
UserId BIGINT NOT NULL,
|
||||||
|
WalletHistoryId BIGINT NOT NULL,
|
||||||
|
TransactionId BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (UserId)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO @Phase1 (UserId, WalletHistoryId, TransactionId) VALUES
|
||||||
|
(75, 102, 23),
|
||||||
|
(93, 103, 24),
|
||||||
|
(96, 104, 25),
|
||||||
|
(99, 105, 26),
|
||||||
|
(47, 106, 28),
|
||||||
|
(43, 107, 29),
|
||||||
|
(45, 108, 30),
|
||||||
|
(52, 122, 31),
|
||||||
|
(116, 138, 42),
|
||||||
|
(115, 139, 43),
|
||||||
|
(119, 140, 44),
|
||||||
|
(113, 141, 45),
|
||||||
|
(103, 142, 46),
|
||||||
|
(170, 292, 107),
|
||||||
|
(171, 293, 108),
|
||||||
|
(172, 294, 109),
|
||||||
|
(175, 295, 110);
|
||||||
|
|
||||||
|
DECLARE @Phase1Count INT = (SELECT COUNT(*) FROM @Phase1);
|
||||||
|
PRINT CONCAT('Phase1 expected rows: ', @Phase1Count);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Validate live DB matches expected mapping
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
p.UserId,
|
||||||
|
u.Mobile,
|
||||||
|
u.FirstName,
|
||||||
|
u.LastName,
|
||||||
|
p.WalletHistoryId AS ExpectedWhId,
|
||||||
|
p.TransactionId AS ExpectedTxId,
|
||||||
|
h.Id AS ActualWhId,
|
||||||
|
h.RefrenceId AS ActualWhRefrenceId,
|
||||||
|
h.ChangeValue AS WhChangeValue,
|
||||||
|
t.Id AS ActualTxId,
|
||||||
|
t.RefId AS TxRefId,
|
||||||
|
t.Amount AS TxAmount,
|
||||||
|
t.PaymentStatus,
|
||||||
|
w.UserId AS WhWalletUserId,
|
||||||
|
CASE
|
||||||
|
WHEN u.Id IS NULL THEN N'USER_MISSING'
|
||||||
|
WHEN h.Id IS NULL THEN N'WH_MISSING'
|
||||||
|
WHEN t.Id IS NULL THEN N'TX_MISSING'
|
||||||
|
WHEN h.RefrenceId <> p.TransactionId THEN N'WH_REF_MISMATCH'
|
||||||
|
WHEN w.UserId <> p.UserId THEN N'WH_WALLET_USER_MISMATCH'
|
||||||
|
WHEN t.RefId NOT LIKE N'manualy%' THEN N'TX_NOT_MANUALY'
|
||||||
|
WHEN t.PaymentStatus <> 0 THEN N'TX_NOT_SUCCESS'
|
||||||
|
ELSE N'OK'
|
||||||
|
END AS ValidationStatus
|
||||||
|
FROM @Phase1 p
|
||||||
|
LEFT JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = p.TransactionId AND t.IsDeleted = 0
|
||||||
|
ORDER BY p.UserId;
|
||||||
|
|
||||||
|
DECLARE @BadValidation INT = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM @Phase1 p
|
||||||
|
LEFT JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = p.TransactionId AND t.IsDeleted = 0
|
||||||
|
WHERE u.Id IS NULL
|
||||||
|
OR h.Id IS NULL
|
||||||
|
OR t.Id IS NULL
|
||||||
|
OR h.RefrenceId <> p.TransactionId
|
||||||
|
OR w.UserId <> p.UserId
|
||||||
|
OR t.RefId NOT LIKE N'manualy%'
|
||||||
|
OR t.PaymentStatus <> 0
|
||||||
|
);
|
||||||
|
|
||||||
|
IF @BadValidation > 0
|
||||||
|
BEGIN
|
||||||
|
DECLARE @BadMsg NVARCHAR(200);
|
||||||
|
SET @BadMsg = CONCAT(
|
||||||
|
N'Validation failed for ', @BadValidation,
|
||||||
|
N' row(s). Fix before creating UPP.'
|
||||||
|
);
|
||||||
|
ROLLBACK;
|
||||||
|
RAISERROR(@BadMsg, 16, 1);
|
||||||
|
RETURN;
|
||||||
|
END
|
||||||
|
|
||||||
|
PRINT 'Validation OK for all 17 rows.';
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Preview: who already has UPP / UPP on this Tx
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
p.UserId,
|
||||||
|
u.Mobile,
|
||||||
|
p.TransactionId,
|
||||||
|
CASE WHEN upp_any.Id IS NOT NULL THEN N'YES' ELSE N'NO' END AS HasAnyUpp,
|
||||||
|
upp_any.Id AS ExistingUppId,
|
||||||
|
upp_any.PurchaseMethod AS ExistingUppMethod,
|
||||||
|
upp_any.TransactionId AS ExistingUppTxId,
|
||||||
|
CASE
|
||||||
|
WHEN upp_tx.Id IS NOT NULL THEN N'SKIP_UPP_EXISTS_FOR_TX'
|
||||||
|
WHEN upp_any.Id IS NOT NULL THEN N'SKIP_USER_HAS_OTHER_UPP'
|
||||||
|
ELSE N'CREATE_UPP'
|
||||||
|
END AS PlanAction
|
||||||
|
FROM @Phase1 p
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1) upp.Id, upp.PurchaseMethod, upp.TransactionId
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0
|
||||||
|
ORDER BY upp.Id DESC
|
||||||
|
) upp_any
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1) upp.Id
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = p.UserId
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
AND upp.TransactionId = p.TransactionId
|
||||||
|
ORDER BY upp.Id DESC
|
||||||
|
) upp_tx
|
||||||
|
ORDER BY p.UserId;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Build UPP insert plan
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
p.UserId,
|
||||||
|
p.TransactionId,
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(cm.LastPackageId, 0),
|
||||||
|
NULLIF(cm.FirstPackageId, 0),
|
||||||
|
@BasePackageId
|
||||||
|
) AS PackageId,
|
||||||
|
COALESCE(t.Amount, @BasePackagePrice) AS Amount,
|
||||||
|
COALESCE(t.PaymentDate, cm.ActivatedAt, @Now) AS PurchasedAt
|
||||||
|
INTO #UppPlan
|
||||||
|
FROM @Phase1 p
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = p.TransactionId AND t.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.ClubMemberships cm ON cm.UserId = p.UserId AND cm.IsDeleted = 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = p.UserId
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
AND upp.TransactionId = p.TransactionId
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE @ToCreate INT = (SELECT COUNT(*) FROM #UppPlan);
|
||||||
|
DECLARE @Skipped INT = (SELECT COUNT(*) FROM @Phase1) - @ToCreate;
|
||||||
|
|
||||||
|
PRINT CONCAT('UPP to create: ', @ToCreate, ' | Already linked (skip): ', @Skipped);
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
pl.UserId,
|
||||||
|
u.Mobile,
|
||||||
|
u.FirstName,
|
||||||
|
u.LastName,
|
||||||
|
pl.TransactionId,
|
||||||
|
pl.PackageId,
|
||||||
|
pl.Amount,
|
||||||
|
pl.PurchasedAt,
|
||||||
|
N'INSERT_UPP_MANUAL_3' AS Action
|
||||||
|
FROM #UppPlan pl
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = pl.UserId AND u.IsDeleted = 0
|
||||||
|
ORDER BY pl.UserId;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Insert UPP
|
||||||
|
------------------------------------------------------------
|
||||||
|
INSERT INTO CMS.UserPackagePurchases
|
||||||
|
(
|
||||||
|
UserId, PackageId, PurchaseMethod, PurchasedAt, Amount,
|
||||||
|
OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
pl.UserId,
|
||||||
|
pl.PackageId,
|
||||||
|
3, -- Manual
|
||||||
|
pl.PurchasedAt,
|
||||||
|
pl.Amount,
|
||||||
|
NULL,
|
||||||
|
pl.TransactionId,
|
||||||
|
@Now,
|
||||||
|
NULL,
|
||||||
|
@Now,
|
||||||
|
NULL,
|
||||||
|
0
|
||||||
|
FROM #UppPlan pl
|
||||||
|
WHERE pl.PackageId IS NOT NULL
|
||||||
|
AND pl.Amount > 0;
|
||||||
|
|
||||||
|
PRINT CONCAT('UPP inserted: ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
DROP TABLE #UppPlan;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Verify
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
p.UserId,
|
||||||
|
u.Mobile,
|
||||||
|
p.TransactionId,
|
||||||
|
CASE WHEN upp.Id IS NOT NULL THEN N'OK' ELSE N'MISSING_UPP' END AS UppStatus,
|
||||||
|
upp.Id AS UppId,
|
||||||
|
upp.PurchaseMethod,
|
||||||
|
upp.TransactionId AS UppTxId
|
||||||
|
FROM @Phase1 p
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserPackagePurchases upp
|
||||||
|
ON upp.UserId = p.UserId
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
AND upp.TransactionId = p.TransactionId
|
||||||
|
ORDER BY p.UserId;
|
||||||
|
|
||||||
|
SELECT N'Missing UPP after run' AS CheckName, COUNT(*) AS Cnt
|
||||||
|
FROM @Phase1 p
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = p.UserId
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
AND upp.TransactionId = p.TransactionId
|
||||||
|
);
|
||||||
|
|
||||||
|
ROLLBACK;
|
||||||
|
-- COMMIT;
|
||||||
|
|
||||||
|
PRINT 'Done (rolled back by default). Uncomment COMMIT when preview looks correct.';
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
/*
|
||||||
|
PHASE 2 — Fix broken WH.RefrenceId + create UPP for 5 remaining manualy txs.
|
||||||
|
|
||||||
|
Tx exists; WH either RefrenceId=0 (CreatedBy = Tx Id hint) or missing (Tx 68).
|
||||||
|
|
||||||
|
Hardcoded mapping (UserWalletHistories2.csv + Transactions.csv):
|
||||||
|
|
||||||
|
UserId WH Tx Notes
|
||||||
|
------ ----- --- -----
|
||||||
|
71 118 56 WH Ref=0, CreatedBy=56
|
||||||
|
72 124 57 WH Ref=0, CreatedBy=57
|
||||||
|
69 119 58 WH Ref=0, CreatedBy=58
|
||||||
|
74 120 59 WH Ref=0, CreatedBy=59
|
||||||
|
142 — 68 no WH — insert history-only (no wallet charge)
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1) Validate WH hint + Tx manualy% + user/wallet
|
||||||
|
2) UPDATE WH.RefrenceId for rows 118,124,119,120
|
||||||
|
3) INSERT UserWalletHistory for User 142 / Tx 68 if missing
|
||||||
|
4) INSERT UserPackagePurchases Manual(3) for all 5 txs
|
||||||
|
|
||||||
|
Does NOT change Users.PackagePurchaseMethod or wallet Balance.
|
||||||
|
|
||||||
|
Defaults to ROLLBACK. Uncomment COMMIT after preview.
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
BEGIN TRAN;
|
||||||
|
|
||||||
|
DECLARE @Now DATETIME2 = SYSUTCDATETIME();
|
||||||
|
|
||||||
|
DECLARE @BasePackageId BIGINT;
|
||||||
|
DECLARE @BasePackagePrice BIGINT;
|
||||||
|
|
||||||
|
SELECT TOP (1)
|
||||||
|
@BasePackageId = Id,
|
||||||
|
@BasePackagePrice = Price
|
||||||
|
FROM CMS.Packages
|
||||||
|
WHERE IsDeleted = 0 AND IsBasePackage = 1
|
||||||
|
ORDER BY Id;
|
||||||
|
|
||||||
|
IF @BasePackageId IS NULL
|
||||||
|
THROW 50001, 'Base package not found.', 1;
|
||||||
|
|
||||||
|
DECLARE @Phase2 TABLE (
|
||||||
|
UserId BIGINT NOT NULL,
|
||||||
|
WalletHistoryId BIGINT NULL,
|
||||||
|
TransactionId BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (TransactionId)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO @Phase2 (UserId, WalletHistoryId, TransactionId) VALUES
|
||||||
|
(71, 118, 56),
|
||||||
|
(72, 124, 57),
|
||||||
|
(69, 119, 58),
|
||||||
|
(74, 120, 59),
|
||||||
|
(142, NULL, 68);
|
||||||
|
|
||||||
|
DECLARE @Phase2Count INT = (SELECT COUNT(*) FROM @Phase2);
|
||||||
|
PRINT CONCAT('Phase2 expected rows: ', @Phase2Count);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 1) Validate before changes
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
p.UserId,
|
||||||
|
u.Mobile AS UserMobile,
|
||||||
|
u.FirstName,
|
||||||
|
u.LastName,
|
||||||
|
p.WalletHistoryId AS ExpectedWhId,
|
||||||
|
p.TransactionId AS ExpectedTxId,
|
||||||
|
h.Id AS ActualWhId,
|
||||||
|
h.RefrenceId AS WhRefrenceId,
|
||||||
|
h.CreatedBy AS WhCreatedBy,
|
||||||
|
h.ChangeValue AS WhChangeValue,
|
||||||
|
w.UserId AS WhWalletUserId,
|
||||||
|
t.Id AS ActualTxId,
|
||||||
|
t.RefId AS TxRefId,
|
||||||
|
t.Amount AS TxAmount,
|
||||||
|
t.CreatedBy AS TxCreatedBy,
|
||||||
|
t.LastModifiedBy AS TxLastModifiedBy,
|
||||||
|
CASE
|
||||||
|
WHEN u.Id IS NULL THEN N'USER_MISSING'
|
||||||
|
WHEN t.Id IS NULL THEN N'TX_MISSING'
|
||||||
|
WHEN t.RefId NOT LIKE N'manualy%' THEN N'TX_NOT_MANUALY'
|
||||||
|
WHEN t.PaymentStatus <> 0 THEN N'TX_NOT_SUCCESS'
|
||||||
|
WHEN p.WalletHistoryId IS NULL THEN N'NO_WH_EXPECTED_CREATE'
|
||||||
|
WHEN h.Id IS NULL THEN N'WH_MISSING'
|
||||||
|
WHEN w.UserId <> p.UserId THEN N'WH_WALLET_USER_MISMATCH'
|
||||||
|
WHEN TRY_CAST(h.CreatedBy AS BIGINT) <> p.TransactionId THEN N'WH_CREATEDBY_HINT_MISMATCH'
|
||||||
|
WHEN h.RefrenceId = p.TransactionId THEN N'WH_ALREADY_LINKED'
|
||||||
|
WHEN h.RefrenceId IS NOT NULL AND h.RefrenceId <> 0 AND h.RefrenceId <> p.TransactionId
|
||||||
|
THEN N'WH_REF_POINTS_OTHER_TX'
|
||||||
|
ELSE N'OK_FIX_REF'
|
||||||
|
END AS WhValidation,
|
||||||
|
CASE
|
||||||
|
WHEN u.Mobile IN (
|
||||||
|
NULLIF(LTRIM(RTRIM(t.CreatedBy)), N''),
|
||||||
|
NULLIF(LTRIM(RTRIM(t.LastModifiedBy)), N'')
|
||||||
|
) THEN N'MOBILE_OK'
|
||||||
|
WHEN NULLIF(LTRIM(RTRIM(t.CreatedBy)), N'') LIKE N'09%' THEN N'TX_MOBILE_USER_MISMATCH'
|
||||||
|
ELSE N'TX_NO_MOBILE_ON_ROW'
|
||||||
|
END AS MobileCheck
|
||||||
|
FROM @Phase2 p
|
||||||
|
LEFT JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = p.TransactionId AND t.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
ORDER BY p.TransactionId;
|
||||||
|
|
||||||
|
DECLARE @BadWh INT = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM @Phase2 p
|
||||||
|
LEFT JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = p.TransactionId AND t.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
|
||||||
|
WHERE t.Id IS NULL
|
||||||
|
OR u.Id IS NULL
|
||||||
|
OR t.RefId NOT LIKE N'manualy%'
|
||||||
|
OR t.PaymentStatus <> 0
|
||||||
|
OR (
|
||||||
|
p.WalletHistoryId IS NOT NULL
|
||||||
|
AND (
|
||||||
|
h.Id IS NULL
|
||||||
|
OR w.UserId <> p.UserId
|
||||||
|
OR TRY_CAST(h.CreatedBy AS BIGINT) <> p.TransactionId
|
||||||
|
OR (h.RefrenceId IS NOT NULL AND h.RefrenceId <> 0 AND h.RefrenceId <> p.TransactionId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
IF @BadWh > 0
|
||||||
|
BEGIN
|
||||||
|
DECLARE @BadMsg NVARCHAR(200);
|
||||||
|
SET @BadMsg = CONCAT(N'Validation failed for ', @BadWh, N' row(s). Aborting.');
|
||||||
|
ROLLBACK;
|
||||||
|
RAISERROR(@BadMsg, 16, 1);
|
||||||
|
RETURN;
|
||||||
|
END
|
||||||
|
|
||||||
|
PRINT 'Validation OK.';
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 2) Fix WH.RefrenceId (118,124,119,120)
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
p.UserId,
|
||||||
|
p.WalletHistoryId,
|
||||||
|
p.TransactionId,
|
||||||
|
h.RefrenceId AS OldRefrenceId,
|
||||||
|
N'UPDATE RefrenceId' AS Action
|
||||||
|
FROM @Phase2 p
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
|
||||||
|
WHERE p.WalletHistoryId IS NOT NULL
|
||||||
|
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0 OR h.RefrenceId <> p.TransactionId);
|
||||||
|
|
||||||
|
UPDATE h
|
||||||
|
SET
|
||||||
|
h.RefrenceId = p.TransactionId,
|
||||||
|
h.LastModified = @Now
|
||||||
|
FROM @Phase2 p
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
|
||||||
|
WHERE p.WalletHistoryId IS NOT NULL
|
||||||
|
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0);
|
||||||
|
|
||||||
|
PRINT CONCAT('WalletHistory RefrenceId updated: ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 3) User 142 / Tx 68 — history-only WH if missing
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
p.UserId,
|
||||||
|
p.TransactionId,
|
||||||
|
w.Id AS WalletId,
|
||||||
|
h.Id AS ExistingWhId,
|
||||||
|
CASE WHEN h.Id IS NOT NULL THEN N'SKIP_WH_EXISTS' ELSE N'CREATE_WH_HISTORY' END AS WhPlan
|
||||||
|
FROM @Phase2 p
|
||||||
|
LEFT JOIN CMS.UserWallets w ON w.UserId = p.UserId AND w.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWalletHistories h ON h.IsDeleted = 0 AND h.RefrenceId = p.TransactionId
|
||||||
|
AND h.WalletId = w.Id
|
||||||
|
WHERE p.WalletHistoryId IS NULL;
|
||||||
|
|
||||||
|
DECLARE @UserId BIGINT = 142;
|
||||||
|
DECLARE @TxId BIGINT = 68;
|
||||||
|
DECLARE @WalletId BIGINT;
|
||||||
|
DECLARE @PackageId BIGINT;
|
||||||
|
DECLARE @Amount BIGINT;
|
||||||
|
DECLARE @HistoryAt DATETIME2;
|
||||||
|
DECLARE @DiscountAmount BIGINT;
|
||||||
|
DECLARE @NetworkBalance BIGINT;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM @Phase2 WHERE TransactionId = 68 AND WalletHistoryId IS NULL)
|
||||||
|
BEGIN
|
||||||
|
SELECT
|
||||||
|
@WalletId = w.Id,
|
||||||
|
@PackageId = COALESCE(
|
||||||
|
NULLIF(cm.LastPackageId, 0),
|
||||||
|
NULLIF(cm.FirstPackageId, 0),
|
||||||
|
@BasePackageId
|
||||||
|
),
|
||||||
|
@Amount = t.Amount,
|
||||||
|
@HistoryAt = COALESCE(t.PaymentDate, @Now),
|
||||||
|
@NetworkBalance = COALESCE(w.NetworkBalance, 0),
|
||||||
|
@DiscountAmount = CAST(ROUND(t.Amount * COALESCE(pkg.DiscountMultiplier, 2.0), 0) AS BIGINT)
|
||||||
|
FROM CMS.Transactions t
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = 142 AND u.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWallets w ON w.UserId = u.Id AND w.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.ClubMemberships cm ON cm.UserId = u.Id AND cm.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Packages pkg ON pkg.Id = COALESCE(
|
||||||
|
NULLIF(cm.LastPackageId, 0),
|
||||||
|
NULLIF(cm.FirstPackageId, 0),
|
||||||
|
@BasePackageId
|
||||||
|
) AND pkg.IsDeleted = 0
|
||||||
|
WHERE t.Id = 68 AND t.IsDeleted = 0;
|
||||||
|
|
||||||
|
IF @WalletId IS NULL
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.UserWallets
|
||||||
|
(UserId, Balance, NetworkBalance, DiscountBalance, WalletMode, Created, LastModified, IsDeleted)
|
||||||
|
VALUES (142, 0, 0, 0, 0, @HistoryAt, @HistoryAt, 0);
|
||||||
|
SET @WalletId = SCOPE_IDENTITY();
|
||||||
|
SET @NetworkBalance = 0;
|
||||||
|
PRINT CONCAT('Wallet created for User 142: ', @WalletId);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
WHERE h.WalletId = @WalletId
|
||||||
|
AND h.IsDeleted = 0
|
||||||
|
AND h.RefrenceId = 68
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.UserWalletHistories
|
||||||
|
(
|
||||||
|
WalletId, CurrentBalance, ChangeValue, CurrentNetworkBalance, ChangeNerworkValue,
|
||||||
|
CurrentDiscountBalance, ChangeDiscountValue, IsIncrease, RefrenceId, PackageId,
|
||||||
|
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@WalletId, 0, @Amount, @NetworkBalance, 0,
|
||||||
|
0, @DiscountAmount, 1, 68, @PackageId,
|
||||||
|
@HistoryAt, NULL, @HistoryAt, NULL, 0
|
||||||
|
);
|
||||||
|
PRINT CONCAT('WalletHistory created for Tx 68 (no charge): WH Id=', SCOPE_IDENTITY());
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
PRINT 'WalletHistory for Tx 68 already exists — skipped.';
|
||||||
|
END
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 4) UPP preview + insert
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
p.UserId,
|
||||||
|
u.Mobile,
|
||||||
|
p.TransactionId,
|
||||||
|
CASE WHEN upp.Id IS NOT NULL THEN N'SKIP_UPP_EXISTS' ELSE N'CREATE_UPP' END AS UppPlan,
|
||||||
|
upp.Id AS ExistingUppId
|
||||||
|
FROM @Phase2 p
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserPackagePurchases upp
|
||||||
|
ON upp.UserId = p.UserId
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
AND upp.TransactionId = p.TransactionId
|
||||||
|
ORDER BY p.TransactionId;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
p.UserId,
|
||||||
|
p.TransactionId,
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(cm.LastPackageId, 0),
|
||||||
|
NULLIF(cm.FirstPackageId, 0),
|
||||||
|
@BasePackageId
|
||||||
|
) AS PackageId,
|
||||||
|
COALESCE(t.Amount, @BasePackagePrice) AS Amount,
|
||||||
|
COALESCE(t.PaymentDate, cm.ActivatedAt, @Now) AS PurchasedAt
|
||||||
|
INTO #UppPlan
|
||||||
|
FROM @Phase2 p
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = p.TransactionId AND t.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.ClubMemberships cm ON cm.UserId = p.UserId AND cm.IsDeleted = 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = p.UserId
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
AND upp.TransactionId = p.TransactionId
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE @UppToCreate INT = (SELECT COUNT(*) FROM #UppPlan);
|
||||||
|
PRINT CONCAT('UPP to create: ', @UppToCreate);
|
||||||
|
|
||||||
|
INSERT INTO CMS.UserPackagePurchases
|
||||||
|
(
|
||||||
|
UserId, PackageId, PurchaseMethod, PurchasedAt, Amount,
|
||||||
|
OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
pl.UserId,
|
||||||
|
pl.PackageId,
|
||||||
|
3,
|
||||||
|
pl.PurchasedAt,
|
||||||
|
pl.Amount,
|
||||||
|
NULL,
|
||||||
|
pl.TransactionId,
|
||||||
|
@Now,
|
||||||
|
NULL,
|
||||||
|
@Now,
|
||||||
|
NULL,
|
||||||
|
0
|
||||||
|
FROM #UppPlan pl
|
||||||
|
WHERE pl.PackageId IS NOT NULL AND pl.Amount > 0;
|
||||||
|
|
||||||
|
PRINT CONCAT('UPP inserted: ', @@ROWCOUNT);
|
||||||
|
DROP TABLE #UppPlan;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 5) Verify
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
p.UserId,
|
||||||
|
u.Mobile,
|
||||||
|
p.TransactionId,
|
||||||
|
h.Id AS WalletHistoryId,
|
||||||
|
h.RefrenceId AS WhRefrenceId,
|
||||||
|
upp.Id AS UppId,
|
||||||
|
upp.PurchaseMethod,
|
||||||
|
CASE
|
||||||
|
WHEN h.Id IS NULL AND p.WalletHistoryId IS NOT NULL THEN N'MISSING_WH'
|
||||||
|
WHEN h.RefrenceId <> p.TransactionId AND p.WalletHistoryId IS NOT NULL THEN N'WH_REF_WRONG'
|
||||||
|
WHEN NOT EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserWalletHistories hx
|
||||||
|
INNER JOIN CMS.UserWallets wx ON wx.Id = hx.WalletId AND wx.UserId = p.UserId
|
||||||
|
WHERE hx.IsDeleted = 0 AND hx.RefrenceId = p.TransactionId
|
||||||
|
) THEN N'NO_WH_FOR_TX'
|
||||||
|
WHEN upp.Id IS NULL THEN N'MISSING_UPP'
|
||||||
|
ELSE N'OK'
|
||||||
|
END AS FinalStatus
|
||||||
|
FROM @Phase2 p
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWalletHistories h ON h.Id = p.WalletHistoryId AND h.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserPackagePurchases upp
|
||||||
|
ON upp.UserId = p.UserId
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
AND upp.TransactionId = p.TransactionId
|
||||||
|
ORDER BY p.TransactionId;
|
||||||
|
|
||||||
|
SELECT N'Incomplete after run' AS CheckName, COUNT(*) AS Cnt
|
||||||
|
FROM @Phase2 p
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0 AND upp.TransactionId = p.TransactionId
|
||||||
|
)
|
||||||
|
OR NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.UserId = p.UserId
|
||||||
|
WHERE h.IsDeleted = 0 AND h.RefrenceId = p.TransactionId
|
||||||
|
);
|
||||||
|
|
||||||
|
ROLLBACK;
|
||||||
|
-- COMMIT;
|
||||||
|
|
||||||
|
PRINT 'Done (rolled back by default). Uncomment COMMIT when preview looks correct.';
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
/*
|
||||||
|
PHASE 3 — Remaining 16 users from WrongDaya→Manual list (no manualy Tx from Phase 1/2).
|
||||||
|
|
||||||
|
Users:
|
||||||
|
2,7,8,9,10,11,12,13,40,41,105,106,130,162,169,176
|
||||||
|
|
||||||
|
Already done in Phase 1+2 (22 users with manualy Tx): excluded here.
|
||||||
|
|
||||||
|
Per user (skip if UPP already exists for that UserId):
|
||||||
|
1) Reuse deposit Tx if WH links to Success Tx with Amount=56000000
|
||||||
|
2) Else fix WH.RefrenceId when CreatedBy hints an existing manualy Tx
|
||||||
|
3) Else CREATE Transaction (RefId FIX3-{UserId}) — no wallet charge
|
||||||
|
4) Link stub WH (Ref=0) to Tx OR insert history-only WH
|
||||||
|
5) INSERT UserPackagePurchases Manual(3)
|
||||||
|
|
||||||
|
User 162: skipped for UPP if any UPP already exists (has UPP from ManualPayment flow).
|
||||||
|
|
||||||
|
Does NOT change Users.PackagePurchaseMethod (Phase 4 later).
|
||||||
|
|
||||||
|
Defaults to ROLLBACK. Uncomment COMMIT after preview.
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
BEGIN TRAN;
|
||||||
|
|
||||||
|
DECLARE @Now DATETIME2 = SYSUTCDATETIME();
|
||||||
|
DECLARE @ChargeAmount BIGINT = 56000000;
|
||||||
|
|
||||||
|
DECLARE @BasePackageId BIGINT;
|
||||||
|
DECLARE @BasePackagePrice BIGINT;
|
||||||
|
|
||||||
|
SELECT TOP (1)
|
||||||
|
@BasePackageId = Id,
|
||||||
|
@BasePackagePrice = Price
|
||||||
|
FROM CMS.Packages
|
||||||
|
WHERE IsDeleted = 0 AND IsBasePackage = 1
|
||||||
|
ORDER BY Id;
|
||||||
|
|
||||||
|
IF @BasePackageId IS NULL
|
||||||
|
THROW 50001, 'Base package not found.', 1;
|
||||||
|
|
||||||
|
DECLARE @Phase3 TABLE (UserId BIGINT PRIMARY KEY);
|
||||||
|
INSERT INTO @Phase3 (UserId) VALUES
|
||||||
|
(2),(7),(8),(9),(10),(11),(12),(13),(40),(41),
|
||||||
|
(105),(106),(130),(162),(169),(176);
|
||||||
|
|
||||||
|
DECLARE @Phase3Count INT = (SELECT COUNT(*) FROM @Phase3);
|
||||||
|
PRINT CONCAT('Phase3 users: ', @Phase3Count);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Preview: current ledger state
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
p.UserId,
|
||||||
|
u.Mobile,
|
||||||
|
u.FirstName,
|
||||||
|
u.LastName,
|
||||||
|
u.PackagePurchaseMethod,
|
||||||
|
(SELECT COUNT(*) FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0) AS UppCount,
|
||||||
|
(SELECT TOP (1) upp.Id FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0 ORDER BY upp.Id DESC) AS LatestUppId,
|
||||||
|
(SELECT COUNT(*) FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
WHERE w.UserId = p.UserId AND h.IsDeleted = 0) AS WhCount,
|
||||||
|
(SELECT COUNT(*) FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
WHERE w.UserId = p.UserId AND h.IsDeleted = 0
|
||||||
|
AND h.ChangeValue >= @ChargeAmount AND h.RefrenceId > 0) AS WhFullDepositCount,
|
||||||
|
CASE
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0
|
||||||
|
) THEN N'SKIP_HAS_UPP'
|
||||||
|
ELSE N'NEEDS_UPP'
|
||||||
|
END AS UppPlan
|
||||||
|
FROM @Phase3 p
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
|
||||||
|
ORDER BY p.UserId;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Resolve Tx per user needing UPP
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
p.UserId,
|
||||||
|
u.Mobile,
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(cm.LastPackageId, 0),
|
||||||
|
NULLIF(cm.FirstPackageId, 0),
|
||||||
|
@BasePackageId
|
||||||
|
) AS PackageId,
|
||||||
|
COALESCE(cm.ActivatedAt, cm.Created, @Now) AS PurchasedAt,
|
||||||
|
dep.TxId AS ExistingDepositTxId,
|
||||||
|
dep.WhId AS ExistingDepositWhId,
|
||||||
|
stub.StubWhId,
|
||||||
|
stub.StubCreatedBy,
|
||||||
|
CAST(NULL AS BIGINT) AS ResolvedTxId,
|
||||||
|
CAST(N'' AS NVARCHAR(40)) AS TxSource,
|
||||||
|
CAST(N'' AS NVARCHAR(40)) AS PlanAction
|
||||||
|
INTO #Plan
|
||||||
|
FROM @Phase3 p
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.ClubMemberships cm ON cm.UserId = p.UserId AND cm.IsDeleted = 0
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1)
|
||||||
|
t.Id AS TxId,
|
||||||
|
h.Id AS WhId
|
||||||
|
FROM CMS.UserWallets w
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = h.RefrenceId
|
||||||
|
AND t.IsDeleted = 0 AND t.PaymentStatus = 0 AND t.Amount = @ChargeAmount
|
||||||
|
WHERE w.UserId = p.UserId AND w.IsDeleted = 0
|
||||||
|
AND h.ChangeValue >= @ChargeAmount
|
||||||
|
AND h.IsIncrease = 1
|
||||||
|
ORDER BY h.Id ASC
|
||||||
|
) dep
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1)
|
||||||
|
h.Id AS StubWhId,
|
||||||
|
h.CreatedBy AS StubCreatedBy
|
||||||
|
FROM CMS.UserWallets w
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
|
||||||
|
WHERE w.UserId = p.UserId AND w.IsDeleted = 0
|
||||||
|
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
|
||||||
|
ORDER BY h.Id ASC
|
||||||
|
) stub
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Pass 1: existing full deposit WH→Tx
|
||||||
|
UPDATE pl
|
||||||
|
SET
|
||||||
|
ResolvedTxId = pl.ExistingDepositTxId,
|
||||||
|
TxSource = N'WH_DEPOSIT',
|
||||||
|
PlanAction = N'CREATE_UPP_REUSE_TX'
|
||||||
|
FROM #Plan pl
|
||||||
|
WHERE pl.ExistingDepositTxId IS NOT NULL;
|
||||||
|
|
||||||
|
-- Pass 2: stub CreatedBy → manualy Tx
|
||||||
|
UPDATE pl
|
||||||
|
SET
|
||||||
|
ResolvedTxId = t.Id,
|
||||||
|
TxSource = N'WH_STUB_HINT',
|
||||||
|
PlanAction = N'FIX_STUB_LINK_UPP'
|
||||||
|
FROM #Plan pl
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(pl.StubCreatedBy AS BIGINT)
|
||||||
|
AND t.IsDeleted = 0 AND t.PaymentStatus = 0 AND t.RefId LIKE N'manualy%'
|
||||||
|
WHERE pl.ResolvedTxId IS NULL
|
||||||
|
AND TRY_CAST(pl.StubCreatedBy AS BIGINT) > 0;
|
||||||
|
|
||||||
|
-- Pass 3: need new Tx
|
||||||
|
UPDATE pl
|
||||||
|
SET
|
||||||
|
TxSource = N'CREATE_TX',
|
||||||
|
PlanAction = N'CREATE_TX_WH_UPP'
|
||||||
|
FROM #Plan pl
|
||||||
|
WHERE pl.ResolvedTxId IS NULL;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
UserId, Mobile, PackageId, PurchasedAt,
|
||||||
|
ExistingDepositTxId, ExistingDepositWhId,
|
||||||
|
StubWhId, StubCreatedBy,
|
||||||
|
ResolvedTxId, TxSource, PlanAction
|
||||||
|
FROM #Plan
|
||||||
|
ORDER BY UserId;
|
||||||
|
|
||||||
|
DECLARE @NeedPlan INT = (SELECT COUNT(*) FROM #Plan);
|
||||||
|
PRINT CONCAT('Users needing UPP in Phase3: ', @NeedPlan);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Execute: cursor per user
|
||||||
|
------------------------------------------------------------
|
||||||
|
DECLARE @UserId BIGINT;
|
||||||
|
DECLARE @Mobile NVARCHAR(50);
|
||||||
|
DECLARE @PackageId BIGINT;
|
||||||
|
DECLARE @PurchasedAt DATETIME2;
|
||||||
|
DECLARE @ExistingDepositTxId BIGINT;
|
||||||
|
DECLARE @StubWhId BIGINT;
|
||||||
|
DECLARE @ResolvedTxId BIGINT;
|
||||||
|
DECLARE @TxSource NVARCHAR(40);
|
||||||
|
DECLARE @TxId BIGINT;
|
||||||
|
DECLARE @WalletId BIGINT;
|
||||||
|
DECLARE @DiscountAmount BIGINT;
|
||||||
|
DECLARE @NetworkBalance BIGINT;
|
||||||
|
DECLARE @HistoryAt DATETIME2;
|
||||||
|
DECLARE @CreatedTx INT = 0;
|
||||||
|
DECLARE @CreatedUpp INT = 0;
|
||||||
|
DECLARE @CreatedWh INT = 0;
|
||||||
|
DECLARE @FixedWh INT = 0;
|
||||||
|
|
||||||
|
DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
|
||||||
|
SELECT UserId, Mobile, PackageId, PurchasedAt,
|
||||||
|
ExistingDepositTxId, StubWhId, ResolvedTxId, TxSource
|
||||||
|
FROM #Plan;
|
||||||
|
|
||||||
|
OPEN cur;
|
||||||
|
FETCH NEXT FROM cur INTO
|
||||||
|
@UserId, @Mobile, @PackageId, @PurchasedAt,
|
||||||
|
@ExistingDepositTxId, @StubWhId, @ResolvedTxId, @TxSource;
|
||||||
|
|
||||||
|
WHILE @@FETCH_STATUS = 0
|
||||||
|
BEGIN
|
||||||
|
SET @TxId = @ResolvedTxId;
|
||||||
|
|
||||||
|
IF @TxId IS NULL
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.Transactions
|
||||||
|
(
|
||||||
|
Amount, Description, PaymentStatus, PaymentDate, RefId, Type,
|
||||||
|
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@ChargeAmount,
|
||||||
|
N'عضویت دستی باشگاه مشتریان - backfill Phase3 WrongDaya',
|
||||||
|
0,
|
||||||
|
@PurchasedAt,
|
||||||
|
CONCAT(N'FIX3-', @UserId),
|
||||||
|
2,
|
||||||
|
@PurchasedAt,
|
||||||
|
@Mobile,
|
||||||
|
@PurchasedAt,
|
||||||
|
@Mobile,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
SET @TxId = SCOPE_IDENTITY();
|
||||||
|
SET @CreatedTx = @CreatedTx + 1;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF @StubWhId IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserWalletHistories h
|
||||||
|
WHERE h.Id = @StubWhId AND h.IsDeleted = 0
|
||||||
|
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
UPDATE CMS.UserWalletHistories
|
||||||
|
SET RefrenceId = @TxId, LastModified = @Now
|
||||||
|
WHERE Id = @StubWhId;
|
||||||
|
SET @FixedWh = @FixedWh + @@ROWCOUNT;
|
||||||
|
END
|
||||||
|
|
||||||
|
SELECT @WalletId = w.Id, @NetworkBalance = COALESCE(w.NetworkBalance, 0)
|
||||||
|
FROM CMS.UserWallets w
|
||||||
|
WHERE w.UserId = @UserId AND w.IsDeleted = 0;
|
||||||
|
|
||||||
|
IF @WalletId IS NULL
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.UserWallets
|
||||||
|
(UserId, Balance, NetworkBalance, DiscountBalance, WalletMode, Created, LastModified, IsDeleted)
|
||||||
|
VALUES (@UserId, 0, 0, 0, 0, @PurchasedAt, @PurchasedAt, 0);
|
||||||
|
SET @WalletId = SCOPE_IDENTITY();
|
||||||
|
SET @NetworkBalance = 0;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserWalletHistories h
|
||||||
|
WHERE h.WalletId = @WalletId AND h.IsDeleted = 0 AND h.RefrenceId = @TxId
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
SELECT
|
||||||
|
@HistoryAt = COALESCE(t.PaymentDate, @PurchasedAt),
|
||||||
|
@DiscountAmount = CAST(ROUND(@ChargeAmount * COALESCE(pkg.DiscountMultiplier, 2.0), 0) AS BIGINT)
|
||||||
|
FROM CMS.Transactions t
|
||||||
|
LEFT JOIN CMS.Packages pkg ON pkg.Id = @PackageId AND pkg.IsDeleted = 0
|
||||||
|
WHERE t.Id = @TxId;
|
||||||
|
|
||||||
|
INSERT INTO CMS.UserWalletHistories
|
||||||
|
(
|
||||||
|
WalletId, CurrentBalance, ChangeValue, CurrentNetworkBalance, ChangeNerworkValue,
|
||||||
|
CurrentDiscountBalance, ChangeDiscountValue, IsIncrease, RefrenceId, PackageId,
|
||||||
|
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@WalletId, 0, @ChargeAmount, @NetworkBalance, 0,
|
||||||
|
0, @DiscountAmount, 1, @TxId, @PackageId,
|
||||||
|
@HistoryAt, NULL, @HistoryAt, NULL, 0
|
||||||
|
);
|
||||||
|
SET @CreatedWh = @CreatedWh + 1;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = @UserId AND upp.IsDeleted = 0 AND upp.TransactionId = @TxId
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.UserPackagePurchases
|
||||||
|
(
|
||||||
|
UserId, PackageId, PurchaseMethod, PurchasedAt, Amount,
|
||||||
|
OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@UserId, @PackageId, 3,
|
||||||
|
@PurchasedAt, @ChargeAmount,
|
||||||
|
NULL, @TxId, @Now, NULL, @Now, NULL, 0
|
||||||
|
);
|
||||||
|
SET @CreatedUpp = @CreatedUpp + 1;
|
||||||
|
END
|
||||||
|
|
||||||
|
FETCH NEXT FROM cur INTO
|
||||||
|
@UserId, @Mobile, @PackageId, @PurchasedAt,
|
||||||
|
@ExistingDepositTxId, @StubWhId, @ResolvedTxId, @TxSource;
|
||||||
|
END
|
||||||
|
|
||||||
|
CLOSE cur;
|
||||||
|
DEALLOCATE cur;
|
||||||
|
DROP TABLE #Plan;
|
||||||
|
|
||||||
|
PRINT CONCAT('Tx created: ', @CreatedTx,
|
||||||
|
' | WH Ref fixed: ', @FixedWh,
|
||||||
|
' | WH created: ', @CreatedWh,
|
||||||
|
' | UPP created: ', @CreatedUpp);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Verify all 16 have UPP
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
p.UserId,
|
||||||
|
u.Mobile,
|
||||||
|
CASE WHEN upp.UppId IS NOT NULL THEN N'OK' ELSE N'MISSING_UPP' END AS UppStatus,
|
||||||
|
upp.UppId,
|
||||||
|
upp.PurchaseMethod,
|
||||||
|
upp.TransactionId AS UppTxId,
|
||||||
|
t.RefId AS TxRefId
|
||||||
|
FROM @Phase3 p
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = p.UserId AND u.IsDeleted = 0
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1)
|
||||||
|
upp2.Id AS UppId,
|
||||||
|
upp2.PurchaseMethod,
|
||||||
|
upp2.TransactionId
|
||||||
|
FROM CMS.UserPackagePurchases upp2
|
||||||
|
WHERE upp2.UserId = p.UserId AND upp2.IsDeleted = 0
|
||||||
|
ORDER BY upp2.Id DESC
|
||||||
|
) upp
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = upp.TransactionId
|
||||||
|
ORDER BY p.UserId;
|
||||||
|
|
||||||
|
SELECT N'Phase3 users still without UPP' AS CheckName, COUNT(*) AS Cnt
|
||||||
|
FROM @Phase3 p
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = p.UserId AND upp.IsDeleted = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
ROLLBACK;
|
||||||
|
-- COMMIT;
|
||||||
|
|
||||||
|
PRINT 'Done (rolled back by default). Uncomment COMMIT when preview looks correct.';
|
||||||
@@ -0,0 +1,740 @@
|
|||||||
|
/*
|
||||||
|
Fix: Manual payment wrongly stored as DirectPurchase (2) → Manual (3)
|
||||||
|
+ create missing Tx/UPP/UserWalletHistory (same ledger as CreateManualPayment service)
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Before commit 427d2cc, CreateManualPayment set Users.PackagePurchaseMethod = DirectPurchase
|
||||||
|
and older rows may lack UserPackagePurchases / wallet history entirely.
|
||||||
|
|
||||||
|
Scope — ALL users present in CMS.ManualPayments (IsDeleted = 0):
|
||||||
|
1) Users / UPP / Club / Cycle: PurchaseMethod 2 → 3
|
||||||
|
2) Users with NO UPP (~18): create Success Tx + UPP Manual(3)
|
||||||
|
3) After that, for EVERY user missing UserWalletHistory on their Tx (~6 existing + ~18 new):
|
||||||
|
- INSERT history only (Created=PaymentDate, ChangeValue=Amount)
|
||||||
|
- DO NOT re-charge wallet
|
||||||
|
- Prefer existing MP/UPP TransactionId (the ~6 already have Tx)
|
||||||
|
|
||||||
|
PackagePurchaseMethod: None=0, DayaLoan=1, DirectPurchase=2, Manual=3
|
||||||
|
ManualPaymentStatus: Pending=0, Approved=1, Rejected=2
|
||||||
|
|
||||||
|
Defaults to ROLLBACK. Uncomment COMMIT after verifying PRINT / preview.
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
BEGIN TRAN;
|
||||||
|
|
||||||
|
DECLARE @Now DATETIME2 = SYSUTCDATETIME();
|
||||||
|
|
||||||
|
DECLARE @BasePackageId BIGINT;
|
||||||
|
DECLARE @BasePackagePrice BIGINT;
|
||||||
|
|
||||||
|
SELECT TOP (1)
|
||||||
|
@BasePackageId = Id,
|
||||||
|
@BasePackagePrice = Price
|
||||||
|
FROM CMS.Packages
|
||||||
|
WHERE IsDeleted = 0 AND IsBasePackage = 1
|
||||||
|
ORDER BY Id;
|
||||||
|
|
||||||
|
IF @BasePackageId IS NULL
|
||||||
|
THROW 50001, 'Base package not found.', 1;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Target: every user that appears in ManualPayments
|
||||||
|
------------------------------------------------------------
|
||||||
|
DECLARE @FixUsers TABLE (UserId BIGINT PRIMARY KEY);
|
||||||
|
|
||||||
|
INSERT INTO @FixUsers (UserId)
|
||||||
|
SELECT DISTINCT mp.UserId
|
||||||
|
FROM CMS.ManualPayments mp
|
||||||
|
WHERE mp.IsDeleted = 0;
|
||||||
|
|
||||||
|
DECLARE @TargetUserCount INT = (SELECT COUNT(*) FROM @FixUsers);
|
||||||
|
PRINT CONCAT('Target users (all ManualPayments): ', @TargetUserCount);
|
||||||
|
PRINT CONCAT('BasePackageId=', @BasePackageId, ' Price=', @BasePackagePrice);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Preview before
|
||||||
|
-- NOTE: UPP row-count ≠ distinct users (one user can have 2+ UPP rows)
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT 'Users DirectPurchase (in scope)' 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 = 2
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'UPP DirectPurchase ROWS (in scope)', COUNT(*)
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
INNER JOIN @FixUsers f ON f.UserId = upp.UserId
|
||||||
|
WHERE upp.IsDeleted = 0 AND upp.PurchaseMethod = 2
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Users with UPP DirectPurchase (distinct)', COUNT(DISTINCT upp.UserId)
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
INNER JOIN @FixUsers f ON f.UserId = upp.UserId
|
||||||
|
WHERE upp.IsDeleted = 0 AND upp.PurchaseMethod = 2
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'ClubMembership DirectPurchase (in scope)', COUNT(*)
|
||||||
|
FROM CMS.ClubMemberships m
|
||||||
|
INNER JOIN @FixUsers f ON f.UserId = m.UserId
|
||||||
|
WHERE m.IsDeleted = 0 AND m.PurchaseMethod = 2
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Cycle DirectPurchase (in scope)', COUNT(*)
|
||||||
|
FROM CMS.ClubMembershipCycles c
|
||||||
|
INNER JOIN @FixUsers f ON f.UserId = c.UserId
|
||||||
|
WHERE c.IsDeleted = 0 AND c.PurchaseMethod = 2
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Users with any UPP (distinct)', COUNT(*)
|
||||||
|
FROM @FixUsers f
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = f.UserId AND upp.IsDeleted = 0
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Missing any UPP (will create Tx+UPP, then WH)', COUNT(*)
|
||||||
|
FROM @FixUsers f
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = f.UserId AND upp.IsDeleted = 0
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Missing WH — have Tx already (create history only)', COUNT(*)
|
||||||
|
FROM @FixUsers f
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1)
|
||||||
|
mp.Id AS ManualPaymentId,
|
||||||
|
mp.TransactionId AS MpTxId,
|
||||||
|
mp.ReferenceNumber
|
||||||
|
FROM CMS.ManualPayments mp
|
||||||
|
WHERE mp.UserId = f.UserId AND mp.IsDeleted = 0
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN mp.Status = 1 THEN 0 ELSE 1 END,
|
||||||
|
CASE WHEN mp.TransactionId IS NOT NULL THEN 0 ELSE 1 END,
|
||||||
|
COALESCE(mp.ApprovedAt, mp.Created) DESC,
|
||||||
|
mp.Id DESC
|
||||||
|
) mp
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1) upp.TransactionId AS UppTxId
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = f.UserId AND upp.IsDeleted = 0 AND upp.TransactionId IS NOT NULL
|
||||||
|
ORDER BY upp.Id DESC
|
||||||
|
) upp
|
||||||
|
CROSS APPLY (
|
||||||
|
SELECT COALESCE(
|
||||||
|
CASE
|
||||||
|
WHEN mp.MpTxId IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM CMS.Transactions t
|
||||||
|
WHERE t.Id = mp.MpTxId AND t.IsDeleted = 0 AND t.PaymentStatus = 0
|
||||||
|
)
|
||||||
|
THEN mp.MpTxId
|
||||||
|
END,
|
||||||
|
upp.UppTxId,
|
||||||
|
(
|
||||||
|
SELECT TOP (1) t.Id
|
||||||
|
FROM CMS.Transactions t
|
||||||
|
WHERE t.IsDeleted = 0 AND t.PaymentStatus = 0
|
||||||
|
AND mp.ReferenceNumber IS NOT NULL
|
||||||
|
AND t.RefId = mp.ReferenceNumber
|
||||||
|
ORDER BY t.Id DESC
|
||||||
|
)
|
||||||
|
) AS TxId
|
||||||
|
) tx
|
||||||
|
WHERE tx.TxId IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
WHERE w.UserId = f.UserId
|
||||||
|
AND h.IsDeleted = 0
|
||||||
|
AND h.RefrenceId = tx.TxId
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Diagnostic: users who already have UPP (expect ~6) — Tx link vs history
|
||||||
|
SELECT
|
||||||
|
f.UserId,
|
||||||
|
mp.ManualPaymentId,
|
||||||
|
mp.MpTxId,
|
||||||
|
upp.UppTxId,
|
||||||
|
tx.TxId AS ResolvedTxId,
|
||||||
|
CASE
|
||||||
|
WHEN tx.TxId IS NULL THEN N'NO_TX_RESOLVED'
|
||||||
|
WHEN h.Id IS NULL THEN N'MISSING_HISTORY'
|
||||||
|
ELSE N'HAS_HISTORY'
|
||||||
|
END AS HistoryStatus,
|
||||||
|
h.Id AS WalletHistoryId
|
||||||
|
FROM @FixUsers f
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1)
|
||||||
|
mp0.Id AS ManualPaymentId,
|
||||||
|
mp0.TransactionId AS MpTxId,
|
||||||
|
mp0.ReferenceNumber
|
||||||
|
FROM CMS.ManualPayments mp0
|
||||||
|
WHERE mp0.UserId = f.UserId AND mp0.IsDeleted = 0
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN mp0.Status = 1 THEN 0 ELSE 1 END,
|
||||||
|
CASE WHEN mp0.TransactionId IS NOT NULL THEN 0 ELSE 1 END,
|
||||||
|
COALESCE(mp0.ApprovedAt, mp0.Created) DESC,
|
||||||
|
mp0.Id DESC
|
||||||
|
) mp
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1) upp0.TransactionId AS UppTxId
|
||||||
|
FROM CMS.UserPackagePurchases upp0
|
||||||
|
WHERE upp0.UserId = f.UserId AND upp0.IsDeleted = 0 AND upp0.TransactionId IS NOT NULL
|
||||||
|
ORDER BY upp0.Id DESC
|
||||||
|
) upp
|
||||||
|
CROSS APPLY (
|
||||||
|
SELECT COALESCE(
|
||||||
|
CASE
|
||||||
|
WHEN mp.MpTxId IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM CMS.Transactions t
|
||||||
|
WHERE t.Id = mp.MpTxId AND t.IsDeleted = 0 AND t.PaymentStatus = 0
|
||||||
|
)
|
||||||
|
THEN mp.MpTxId
|
||||||
|
END,
|
||||||
|
upp.UppTxId,
|
||||||
|
(
|
||||||
|
SELECT TOP (1) t.Id
|
||||||
|
FROM CMS.Transactions t
|
||||||
|
WHERE t.IsDeleted = 0 AND t.PaymentStatus = 0
|
||||||
|
AND mp.ReferenceNumber IS NOT NULL
|
||||||
|
AND t.RefId = mp.ReferenceNumber
|
||||||
|
ORDER BY t.Id DESC
|
||||||
|
)
|
||||||
|
) AS TxId
|
||||||
|
) tx
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1) h0.Id
|
||||||
|
FROM CMS.UserWalletHistories h0
|
||||||
|
INNER JOIN CMS.UserWallets w0 ON w0.Id = h0.WalletId AND w0.IsDeleted = 0
|
||||||
|
WHERE w0.UserId = f.UserId AND h0.IsDeleted = 0 AND h0.RefrenceId = tx.TxId
|
||||||
|
) h
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases uany
|
||||||
|
WHERE uany.UserId = f.UserId AND uany.IsDeleted = 0
|
||||||
|
)
|
||||||
|
ORDER BY f.UserId;
|
||||||
|
|
||||||
|
-- Who has more than one UPP row? (explains ROWS vs users)
|
||||||
|
SELECT
|
||||||
|
upp.UserId,
|
||||||
|
COUNT(*) AS UppRowCnt,
|
||||||
|
SUM(CASE WHEN upp.PurchaseMethod = 2 THEN 1 ELSE 0 END) AS UppDirectPurchaseCnt,
|
||||||
|
SUM(CASE WHEN upp.PurchaseMethod = 3 THEN 1 ELSE 0 END) AS UppManualCnt
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
INNER JOIN @FixUsers f ON f.UserId = upp.UserId
|
||||||
|
WHERE upp.IsDeleted = 0
|
||||||
|
GROUP BY upp.UserId
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
ORDER BY UppRowCnt DESC, upp.UserId;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 1) Users 2 → 3
|
||||||
|
------------------------------------------------------------
|
||||||
|
UPDATE u
|
||||||
|
SET
|
||||||
|
u.PackagePurchaseMethod = 3,
|
||||||
|
u.LastModified = @Now
|
||||||
|
FROM CMS.Users u
|
||||||
|
INNER JOIN @FixUsers f ON f.UserId = u.Id
|
||||||
|
WHERE u.IsDeleted = 0
|
||||||
|
AND u.PackagePurchaseMethod = 2;
|
||||||
|
|
||||||
|
PRINT CONCAT('Users updated 2→3: ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 2) Existing UPP 2 → 3
|
||||||
|
------------------------------------------------------------
|
||||||
|
UPDATE upp
|
||||||
|
SET
|
||||||
|
upp.PurchaseMethod = 3,
|
||||||
|
upp.LastModified = @Now
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
INNER JOIN @FixUsers f ON f.UserId = upp.UserId
|
||||||
|
WHERE upp.IsDeleted = 0
|
||||||
|
AND upp.PurchaseMethod = 2;
|
||||||
|
|
||||||
|
PRINT CONCAT('UserPackagePurchases updated 2→3: ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 3) ClubMemberships 2 → 3
|
||||||
|
------------------------------------------------------------
|
||||||
|
UPDATE m
|
||||||
|
SET
|
||||||
|
m.PurchaseMethod = 3,
|
||||||
|
m.LastModified = @Now
|
||||||
|
FROM CMS.ClubMemberships m
|
||||||
|
INNER JOIN @FixUsers f ON f.UserId = m.UserId
|
||||||
|
WHERE m.IsDeleted = 0
|
||||||
|
AND m.PurchaseMethod = 2;
|
||||||
|
|
||||||
|
PRINT CONCAT('ClubMemberships updated 2→3: ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 4) ClubMembershipCycles 2 → 3
|
||||||
|
------------------------------------------------------------
|
||||||
|
UPDATE c
|
||||||
|
SET
|
||||||
|
c.PurchaseMethod = 3,
|
||||||
|
c.LastModified = @Now
|
||||||
|
FROM CMS.ClubMembershipCycles c
|
||||||
|
INNER JOIN @FixUsers f ON f.UserId = c.UserId
|
||||||
|
WHERE c.IsDeleted = 0
|
||||||
|
AND c.PurchaseMethod = 2;
|
||||||
|
|
||||||
|
PRINT CONCAT('ClubMembershipCycles updated 2→3: ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 5) Create missing Tx + UPP (like CreateManualPayment ledger)
|
||||||
|
------------------------------------------------------------
|
||||||
|
;WITH MissingUppUsers AS (
|
||||||
|
-- Any non-deleted UPP counts as present (avoid duplicate for method=2 / broken Tx)
|
||||||
|
SELECT f.UserId
|
||||||
|
FROM @FixUsers f
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = f.UserId
|
||||||
|
AND upp.IsDeleted = 0
|
||||||
|
)
|
||||||
|
),
|
||||||
|
BestManual AS (
|
||||||
|
SELECT
|
||||||
|
mp.UserId,
|
||||||
|
mp.Id AS ManualPaymentId,
|
||||||
|
mp.Amount,
|
||||||
|
mp.TransactionId,
|
||||||
|
mp.ReferenceNumber,
|
||||||
|
mp.Description,
|
||||||
|
COALESCE(mp.ApprovedAt, mp.Created) AS PurchasedAt,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY mp.UserId
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN mp.Status = 1 THEN 0 ELSE 1 END, -- Approved first
|
||||||
|
CASE WHEN mp.TransactionId IS NOT NULL THEN 0 ELSE 1 END,
|
||||||
|
COALESCE(mp.ApprovedAt, mp.Created) DESC,
|
||||||
|
mp.Id DESC
|
||||||
|
) AS rn
|
||||||
|
FROM CMS.ManualPayments mp
|
||||||
|
INNER JOIN MissingUppUsers m ON m.UserId = mp.UserId
|
||||||
|
WHERE mp.IsDeleted = 0
|
||||||
|
),
|
||||||
|
Candidates AS (
|
||||||
|
SELECT
|
||||||
|
bm.UserId,
|
||||||
|
bm.ManualPaymentId,
|
||||||
|
bm.TransactionId AS ExistingTxId,
|
||||||
|
bm.ReferenceNumber,
|
||||||
|
bm.Description,
|
||||||
|
bm.PurchasedAt,
|
||||||
|
COALESCE(NULLIF(cm.LastPackageId, 0), NULLIF(cm.FirstPackageId, 0), @BasePackageId) AS PackageId,
|
||||||
|
CASE
|
||||||
|
WHEN bm.Amount IS NOT NULL AND bm.Amount > 0 THEN bm.Amount
|
||||||
|
ELSE COALESCE(p.Price, @BasePackagePrice)
|
||||||
|
END AS Amount
|
||||||
|
FROM BestManual bm
|
||||||
|
LEFT JOIN CMS.ClubMemberships cm
|
||||||
|
ON cm.UserId = bm.UserId AND cm.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Packages p
|
||||||
|
ON p.Id = COALESCE(NULLIF(cm.LastPackageId, 0), NULLIF(cm.FirstPackageId, 0), @BasePackageId)
|
||||||
|
AND p.IsDeleted = 0
|
||||||
|
WHERE bm.rn = 1
|
||||||
|
)
|
||||||
|
SELECT *
|
||||||
|
INTO #UppToCreate
|
||||||
|
FROM Candidates
|
||||||
|
WHERE PackageId IS NOT NULL AND Amount > 0;
|
||||||
|
|
||||||
|
DECLARE @MissingUppCount INT = (SELECT COUNT(*) FROM #UppToCreate);
|
||||||
|
PRINT CONCAT('Missing UPP candidates to create: ', @MissingUppCount);
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
UserId,
|
||||||
|
ManualPaymentId,
|
||||||
|
ExistingTxId,
|
||||||
|
PackageId,
|
||||||
|
Amount,
|
||||||
|
PurchasedAt,
|
||||||
|
CASE WHEN ExistingTxId IS NOT NULL THEN N'REUSE_MP_TX' ELSE N'CREATE_NEW_TX' END AS TxPlan
|
||||||
|
FROM #UppToCreate
|
||||||
|
ORDER BY UserId;
|
||||||
|
|
||||||
|
DECLARE @UserId BIGINT;
|
||||||
|
DECLARE @ManualPaymentId BIGINT;
|
||||||
|
DECLARE @ExistingTxId BIGINT;
|
||||||
|
DECLARE @PackageId BIGINT;
|
||||||
|
DECLARE @Amount BIGINT;
|
||||||
|
DECLARE @PurchasedAt DATETIME2;
|
||||||
|
DECLARE @ReferenceNumber NVARCHAR(200);
|
||||||
|
DECLARE @Description NVARCHAR(500);
|
||||||
|
DECLARE @TxId BIGINT;
|
||||||
|
DECLARE @CreatedUpp INT = 0;
|
||||||
|
DECLARE @CreatedTx INT = 0;
|
||||||
|
DECLARE @ReusedTx INT = 0;
|
||||||
|
|
||||||
|
DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
|
||||||
|
SELECT UserId, ManualPaymentId, ExistingTxId, PackageId, Amount, PurchasedAt, ReferenceNumber, Description
|
||||||
|
FROM #UppToCreate;
|
||||||
|
|
||||||
|
OPEN cur;
|
||||||
|
FETCH NEXT FROM cur INTO
|
||||||
|
@UserId, @ManualPaymentId, @ExistingTxId, @PackageId, @Amount, @PurchasedAt, @ReferenceNumber, @Description;
|
||||||
|
|
||||||
|
WHILE @@FETCH_STATUS = 0
|
||||||
|
BEGIN
|
||||||
|
SET @TxId = NULL;
|
||||||
|
|
||||||
|
-- Reuse Success Tx from ManualPayment when possible
|
||||||
|
IF @ExistingTxId IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM CMS.Transactions t
|
||||||
|
WHERE t.Id = @ExistingTxId AND t.IsDeleted = 0 AND t.PaymentStatus = 0
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
SET @TxId = @ExistingTxId;
|
||||||
|
SET @ReusedTx = @ReusedTx + 1;
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.Transactions
|
||||||
|
(
|
||||||
|
Amount, Description, PaymentStatus, PaymentDate, RefId, Type,
|
||||||
|
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@Amount,
|
||||||
|
CONCAT(
|
||||||
|
N'عضویت دستی باشگاه مشتریان - ',
|
||||||
|
ISNULL(@Description, N'backfill UPP'),
|
||||||
|
N' - مرجع: ',
|
||||||
|
ISNULL(@ReferenceNumber, CONCAT(N'MP-', @ManualPaymentId))
|
||||||
|
),
|
||||||
|
0, -- Success
|
||||||
|
@PurchasedAt,
|
||||||
|
ISNULL(@ReferenceNumber, CONCAT(N'UPP-MP-FIX-', @ManualPaymentId)),
|
||||||
|
2, -- DepositExternal1
|
||||||
|
@PurchasedAt, NULL, @PurchasedAt, NULL, 0
|
||||||
|
);
|
||||||
|
SET @TxId = SCOPE_IDENTITY();
|
||||||
|
SET @CreatedTx = @CreatedTx + 1;
|
||||||
|
|
||||||
|
UPDATE CMS.ManualPayments
|
||||||
|
SET
|
||||||
|
TransactionId = @TxId,
|
||||||
|
LastModified = @Now
|
||||||
|
WHERE Id = @ManualPaymentId
|
||||||
|
AND (TransactionId IS NULL OR TransactionId <> @TxId);
|
||||||
|
END
|
||||||
|
|
||||||
|
INSERT INTO CMS.UserPackagePurchases
|
||||||
|
(
|
||||||
|
UserId, PackageId, PurchaseMethod, PurchasedAt, Amount,
|
||||||
|
OrderId, TransactionId, Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@UserId, @PackageId, 3, -- Manual
|
||||||
|
@PurchasedAt, @Amount,
|
||||||
|
NULL, @TxId, @PurchasedAt, NULL, @PurchasedAt, NULL, 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Align user flag (only None/DirectPurchase → Manual; leave real Daya=1 alone)
|
||||||
|
UPDATE CMS.Users
|
||||||
|
SET
|
||||||
|
PackagePurchaseMethod = 3,
|
||||||
|
LastModified = @Now
|
||||||
|
WHERE Id = @UserId
|
||||||
|
AND IsDeleted = 0
|
||||||
|
AND PackagePurchaseMethod IN (0, 2);
|
||||||
|
|
||||||
|
SET @CreatedUpp = @CreatedUpp + 1;
|
||||||
|
|
||||||
|
FETCH NEXT FROM cur INTO
|
||||||
|
@UserId, @ManualPaymentId, @ExistingTxId, @PackageId, @Amount, @PurchasedAt, @ReferenceNumber, @Description;
|
||||||
|
END
|
||||||
|
|
||||||
|
CLOSE cur;
|
||||||
|
DEALLOCATE cur;
|
||||||
|
|
||||||
|
DROP TABLE #UppToCreate;
|
||||||
|
|
||||||
|
PRINT CONCAT('UPP created: ', @CreatedUpp,
|
||||||
|
' | Tx reused: ', @ReusedTx,
|
||||||
|
' | Tx created: ', @CreatedTx);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 6) UserWalletHistory for every FixUser that has a Tx but no history
|
||||||
|
-- Runs AFTER step 5 so the ~18 new Txs are included.
|
||||||
|
-- The ~6 with existing UPP/Tx only get history (no new Tx if resolved).
|
||||||
|
-- NO wallet Balance/Discount charge.
|
||||||
|
------------------------------------------------------------
|
||||||
|
;WITH BestManualAll AS (
|
||||||
|
SELECT
|
||||||
|
mp.UserId,
|
||||||
|
mp.Id AS ManualPaymentId,
|
||||||
|
mp.Amount,
|
||||||
|
mp.TransactionId,
|
||||||
|
mp.ReferenceNumber,
|
||||||
|
mp.Description,
|
||||||
|
COALESCE(mp.ApprovedAt, mp.Created) AS PurchasedAt,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY mp.UserId
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN mp.Status = 1 THEN 0 ELSE 1 END,
|
||||||
|
CASE WHEN mp.TransactionId IS NOT NULL THEN 0 ELSE 1 END,
|
||||||
|
COALESCE(mp.ApprovedAt, mp.Created) DESC,
|
||||||
|
mp.Id DESC
|
||||||
|
) AS rn
|
||||||
|
FROM CMS.ManualPayments mp
|
||||||
|
INNER JOIN @FixUsers f ON f.UserId = mp.UserId
|
||||||
|
WHERE mp.IsDeleted = 0
|
||||||
|
),
|
||||||
|
Resolved AS (
|
||||||
|
SELECT
|
||||||
|
bm.UserId,
|
||||||
|
bm.ManualPaymentId,
|
||||||
|
bm.ReferenceNumber,
|
||||||
|
bm.Description,
|
||||||
|
bm.PurchasedAt,
|
||||||
|
COALESCE(NULLIF(cm.LastPackageId, 0), NULLIF(cm.FirstPackageId, 0), @BasePackageId) AS PackageId,
|
||||||
|
CASE
|
||||||
|
WHEN bm.Amount IS NOT NULL AND bm.Amount > 0 THEN bm.Amount
|
||||||
|
ELSE COALESCE(p.Price, @BasePackagePrice)
|
||||||
|
END AS Amount,
|
||||||
|
COALESCE(
|
||||||
|
CASE
|
||||||
|
WHEN bm.TransactionId IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM CMS.Transactions t
|
||||||
|
WHERE t.Id = bm.TransactionId AND t.IsDeleted = 0 AND t.PaymentStatus = 0
|
||||||
|
)
|
||||||
|
THEN bm.TransactionId
|
||||||
|
END,
|
||||||
|
(
|
||||||
|
SELECT TOP (1) upp.TransactionId
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = bm.UserId AND upp.IsDeleted = 0 AND upp.TransactionId IS NOT NULL
|
||||||
|
ORDER BY upp.Id DESC
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SELECT TOP (1) t.Id
|
||||||
|
FROM CMS.Transactions t
|
||||||
|
WHERE t.IsDeleted = 0 AND t.PaymentStatus = 0
|
||||||
|
AND bm.ReferenceNumber IS NOT NULL
|
||||||
|
AND t.RefId = bm.ReferenceNumber
|
||||||
|
ORDER BY t.Id DESC
|
||||||
|
)
|
||||||
|
) AS TxId
|
||||||
|
FROM BestManualAll bm
|
||||||
|
LEFT JOIN CMS.ClubMemberships cm
|
||||||
|
ON cm.UserId = bm.UserId AND cm.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Packages p
|
||||||
|
ON p.Id = COALESCE(NULLIF(cm.LastPackageId, 0), NULLIF(cm.FirstPackageId, 0), @BasePackageId)
|
||||||
|
AND p.IsDeleted = 0
|
||||||
|
WHERE bm.rn = 1
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
r.UserId,
|
||||||
|
r.ManualPaymentId,
|
||||||
|
r.TxId,
|
||||||
|
r.PackageId,
|
||||||
|
r.Amount,
|
||||||
|
COALESCE(t.PaymentDate, r.PurchasedAt) AS HistoryAt,
|
||||||
|
CAST(ROUND(r.Amount * COALESCE(p.DiscountMultiplier, 2.0), 0) AS BIGINT) AS DiscountAmount,
|
||||||
|
w.Id AS WalletId,
|
||||||
|
COALESCE(w.NetworkBalance, 0) AS NetworkBalance
|
||||||
|
INTO #WalletHistoryToCreate
|
||||||
|
FROM Resolved r
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = r.TxId AND t.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Packages p ON p.Id = r.PackageId AND p.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWallets w ON w.UserId = r.UserId AND w.IsDeleted = 0
|
||||||
|
WHERE r.TxId IS NOT NULL
|
||||||
|
AND r.Amount > 0
|
||||||
|
AND r.PackageId IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets wh ON wh.Id = h.WalletId AND wh.IsDeleted = 0
|
||||||
|
WHERE wh.UserId = r.UserId
|
||||||
|
AND h.IsDeleted = 0
|
||||||
|
AND h.RefrenceId = r.TxId
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE @MissingWhCount INT = (SELECT COUNT(*) FROM #WalletHistoryToCreate);
|
||||||
|
PRINT CONCAT('Missing WalletHistory (after UPP/Tx step — expect ~6 old + ~18 new): ', @MissingWhCount);
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
UserId, ManualPaymentId, TxId, PackageId, Amount, DiscountAmount, HistoryAt, WalletId,
|
||||||
|
N'CREATE_HISTORY_ONLY' AS PlanAction
|
||||||
|
FROM #WalletHistoryToCreate
|
||||||
|
ORDER BY UserId;
|
||||||
|
|
||||||
|
DECLARE @WalletId BIGINT;
|
||||||
|
DECLARE @DiscountAmount BIGINT;
|
||||||
|
DECLARE @HistoryAt DATETIME2;
|
||||||
|
DECLARE @NetworkBalance BIGINT;
|
||||||
|
DECLARE @CreatedWh INT = 0;
|
||||||
|
|
||||||
|
DECLARE curWh CURSOR LOCAL FAST_FORWARD FOR
|
||||||
|
SELECT UserId, ManualPaymentId, TxId, PackageId, Amount, DiscountAmount, HistoryAt, WalletId, NetworkBalance
|
||||||
|
FROM #WalletHistoryToCreate;
|
||||||
|
|
||||||
|
OPEN curWh;
|
||||||
|
FETCH NEXT FROM curWh INTO
|
||||||
|
@UserId, @ManualPaymentId, @TxId, @PackageId, @Amount, @DiscountAmount, @HistoryAt, @WalletId, @NetworkBalance;
|
||||||
|
|
||||||
|
WHILE @@FETCH_STATUS = 0
|
||||||
|
BEGIN
|
||||||
|
-- Keep MP linked to resolved Tx when it was found via UPP / RefId
|
||||||
|
UPDATE CMS.ManualPayments
|
||||||
|
SET TransactionId = @TxId, LastModified = @Now
|
||||||
|
WHERE Id = @ManualPaymentId
|
||||||
|
AND (TransactionId IS NULL OR TransactionId <> @TxId);
|
||||||
|
|
||||||
|
IF @WalletId IS NULL
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO CMS.UserWallets
|
||||||
|
(UserId, Balance, NetworkBalance, DiscountBalance, WalletMode, Created, LastModified, IsDeleted)
|
||||||
|
VALUES
|
||||||
|
(@UserId, 0, 0, 0, 0, @HistoryAt, @HistoryAt, 0);
|
||||||
|
SET @WalletId = SCOPE_IDENTITY();
|
||||||
|
SET @NetworkBalance = 0;
|
||||||
|
END
|
||||||
|
|
||||||
|
INSERT INTO CMS.UserWalletHistories
|
||||||
|
(
|
||||||
|
WalletId, CurrentBalance, ChangeValue, CurrentNetworkBalance, ChangeNerworkValue,
|
||||||
|
CurrentDiscountBalance, ChangeDiscountValue, IsIncrease, RefrenceId, PackageId,
|
||||||
|
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@WalletId,
|
||||||
|
0,
|
||||||
|
@Amount,
|
||||||
|
@NetworkBalance,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
@DiscountAmount,
|
||||||
|
1,
|
||||||
|
@TxId,
|
||||||
|
@PackageId,
|
||||||
|
@HistoryAt,
|
||||||
|
NULL,
|
||||||
|
@HistoryAt,
|
||||||
|
NULL,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @CreatedWh = @CreatedWh + 1;
|
||||||
|
|
||||||
|
FETCH NEXT FROM curWh INTO
|
||||||
|
@UserId, @ManualPaymentId, @TxId, @PackageId, @Amount, @DiscountAmount, @HistoryAt, @WalletId, @NetworkBalance;
|
||||||
|
END
|
||||||
|
|
||||||
|
CLOSE curWh;
|
||||||
|
DEALLOCATE curWh;
|
||||||
|
|
||||||
|
DROP TABLE #WalletHistoryToCreate;
|
||||||
|
|
||||||
|
PRINT CONCAT('WalletHistory created (no charge): ', @CreatedWh);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Verify after
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT 'Users still DirectPurchase (in scope)' 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 = 2
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'UPP still DirectPurchase (in scope)', COUNT(*)
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
INNER JOIN @FixUsers f ON f.UserId = upp.UserId
|
||||||
|
WHERE upp.IsDeleted = 0 AND upp.PurchaseMethod = 2
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Club still DirectPurchase (in scope)', COUNT(*)
|
||||||
|
FROM CMS.ClubMemberships m
|
||||||
|
INNER JOIN @FixUsers f ON f.UserId = m.UserId
|
||||||
|
WHERE m.IsDeleted = 0 AND m.PurchaseMethod = 2
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Cycle still DirectPurchase (in scope)', COUNT(*)
|
||||||
|
FROM CMS.ClubMembershipCycles c
|
||||||
|
INNER JOIN @FixUsers f ON f.UserId = c.UserId
|
||||||
|
WHERE c.IsDeleted = 0 AND c.PurchaseMethod = 2
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Still missing any UPP (in scope)', COUNT(*)
|
||||||
|
FROM @FixUsers f
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = f.UserId AND upp.IsDeleted = 0
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Still missing WalletHistory for resolved Tx', COUNT(*)
|
||||||
|
FROM @FixUsers f
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1)
|
||||||
|
mp.TransactionId AS MpTxId,
|
||||||
|
mp.ReferenceNumber
|
||||||
|
FROM CMS.ManualPayments mp
|
||||||
|
WHERE mp.UserId = f.UserId AND mp.IsDeleted = 0
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN mp.Status = 1 THEN 0 ELSE 1 END,
|
||||||
|
CASE WHEN mp.TransactionId IS NOT NULL THEN 0 ELSE 1 END,
|
||||||
|
COALESCE(mp.ApprovedAt, mp.Created) DESC,
|
||||||
|
mp.Id DESC
|
||||||
|
) mp
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1) upp.TransactionId AS UppTxId
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = f.UserId AND upp.IsDeleted = 0 AND upp.TransactionId IS NOT NULL
|
||||||
|
ORDER BY upp.Id DESC
|
||||||
|
) upp
|
||||||
|
CROSS APPLY (
|
||||||
|
SELECT COALESCE(
|
||||||
|
CASE
|
||||||
|
WHEN mp.MpTxId IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM CMS.Transactions t
|
||||||
|
WHERE t.Id = mp.MpTxId AND t.IsDeleted = 0 AND t.PaymentStatus = 0
|
||||||
|
)
|
||||||
|
THEN mp.MpTxId
|
||||||
|
END,
|
||||||
|
upp.UppTxId,
|
||||||
|
(
|
||||||
|
SELECT TOP (1) t.Id
|
||||||
|
FROM CMS.Transactions t
|
||||||
|
WHERE t.IsDeleted = 0 AND t.PaymentStatus = 0
|
||||||
|
AND mp.ReferenceNumber IS NOT NULL
|
||||||
|
AND t.RefId = mp.ReferenceNumber
|
||||||
|
ORDER BY t.Id DESC
|
||||||
|
)
|
||||||
|
) AS TxId
|
||||||
|
) tx
|
||||||
|
WHERE tx.TxId IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
WHERE w.UserId = f.UserId
|
||||||
|
AND h.IsDeleted = 0
|
||||||
|
AND h.RefrenceId = tx.TxId
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT TOP (50)
|
||||||
|
u.Id AS UserId,
|
||||||
|
u.Mobile,
|
||||||
|
u.PackagePurchaseMethod AS UserMethod,
|
||||||
|
m.PurchaseMethod AS ClubMethod,
|
||||||
|
(SELECT COUNT(*) FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = u.Id AND upp.IsDeleted = 0 AND upp.PurchaseMethod = 3) AS UppManualCnt
|
||||||
|
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.';
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/*
|
||||||
|
Backfill UserWalletHistory for 6 ManualPayment users (history only — NO wallet charge)
|
||||||
|
|
||||||
|
Transaction IDs:
|
||||||
|
172, 40182, 40184, 40201, 40264, 110556
|
||||||
|
|
||||||
|
Wallet was already charged when ManualPayment ran; this script only inserts
|
||||||
|
the missing UserWalletHistory row (same shape as CreateManualPayment).
|
||||||
|
|
||||||
|
Defaults to ROLLBACK. Uncomment COMMIT after verifying preview.
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
BEGIN TRAN;
|
||||||
|
|
||||||
|
DECLARE @Now DATETIME2 = SYSUTCDATETIME();
|
||||||
|
|
||||||
|
DECLARE @BasePackageId BIGINT;
|
||||||
|
DECLARE @BasePackagePrice BIGINT;
|
||||||
|
|
||||||
|
SELECT TOP (1)
|
||||||
|
@BasePackageId = Id,
|
||||||
|
@BasePackagePrice = Price
|
||||||
|
FROM CMS.Packages
|
||||||
|
WHERE IsDeleted = 0 AND IsBasePackage = 1
|
||||||
|
ORDER BY Id;
|
||||||
|
|
||||||
|
IF @BasePackageId IS NULL
|
||||||
|
THROW 50001, 'Base package not found.', 1;
|
||||||
|
|
||||||
|
DECLARE @TargetTx TABLE (TransactionId BIGINT PRIMARY KEY);
|
||||||
|
|
||||||
|
INSERT INTO @TargetTx (TransactionId)
|
||||||
|
VALUES
|
||||||
|
(172),
|
||||||
|
(40182),
|
||||||
|
(40184),
|
||||||
|
(40201),
|
||||||
|
(40264),
|
||||||
|
(110556);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Preview
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
tx.TransactionId,
|
||||||
|
t.Amount AS TxAmount,
|
||||||
|
t.PaymentDate,
|
||||||
|
t.PaymentStatus,
|
||||||
|
t.IsDeleted AS TxDeleted,
|
||||||
|
COALESCE(mp.UserId, upp.UserId) AS UserId,
|
||||||
|
mp.Id AS ManualPaymentId,
|
||||||
|
upp.Id AS UserPackagePurchaseId,
|
||||||
|
COALESCE(
|
||||||
|
upp.PackageId,
|
||||||
|
NULLIF(cm.LastPackageId, 0),
|
||||||
|
NULLIF(cm.FirstPackageId, 0),
|
||||||
|
@BasePackageId
|
||||||
|
) AS PackageId,
|
||||||
|
w.Id AS WalletId,
|
||||||
|
h.Id AS ExistingWalletHistoryId,
|
||||||
|
CASE
|
||||||
|
WHEN t.Id IS NULL THEN N'TX_NOT_FOUND'
|
||||||
|
WHEN t.IsDeleted = 1 THEN N'TX_DELETED'
|
||||||
|
WHEN t.PaymentStatus <> 0 THEN N'TX_NOT_SUCCESS'
|
||||||
|
WHEN COALESCE(mp.UserId, upp.UserId) IS NULL THEN N'USER_NOT_RESOLVED'
|
||||||
|
WHEN w.Id IS NULL THEN N'WALLET_MISSING'
|
||||||
|
WHEN h.Id IS NOT NULL THEN N'ALREADY_HAS_HISTORY'
|
||||||
|
ELSE N'WILL_CREATE_HISTORY'
|
||||||
|
END AS PlanAction
|
||||||
|
FROM @TargetTx tx
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = tx.TransactionId
|
||||||
|
LEFT JOIN CMS.ManualPayments mp
|
||||||
|
ON mp.TransactionId = tx.TransactionId AND mp.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserPackagePurchases upp
|
||||||
|
ON upp.TransactionId = tx.TransactionId AND upp.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.ClubMemberships cm
|
||||||
|
ON cm.UserId = COALESCE(mp.UserId, upp.UserId) AND cm.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWallets w
|
||||||
|
ON w.UserId = COALESCE(mp.UserId, upp.UserId) AND w.IsDeleted = 0
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1) h0.Id
|
||||||
|
FROM CMS.UserWalletHistories h0
|
||||||
|
WHERE h0.IsDeleted = 0
|
||||||
|
AND h0.RefrenceId = tx.TransactionId
|
||||||
|
AND (
|
||||||
|
w.Id IS NULL
|
||||||
|
OR h0.WalletId = w.Id
|
||||||
|
)
|
||||||
|
) h
|
||||||
|
ORDER BY tx.TransactionId;
|
||||||
|
|
||||||
|
SELECT 'Will create WalletHistory' AS CheckName, COUNT(*) AS Cnt
|
||||||
|
FROM @TargetTx tx
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = tx.TransactionId AND t.IsDeleted = 0 AND t.PaymentStatus = 0
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1) mp.UserId
|
||||||
|
FROM CMS.ManualPayments mp
|
||||||
|
WHERE mp.TransactionId = tx.TransactionId AND mp.IsDeleted = 0
|
||||||
|
) mp
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1) upp.UserId, upp.PackageId
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.TransactionId = tx.TransactionId AND upp.IsDeleted = 0
|
||||||
|
ORDER BY upp.Id DESC
|
||||||
|
) upp
|
||||||
|
CROSS APPLY (
|
||||||
|
SELECT COALESCE(mp.UserId, upp.UserId) AS UserId
|
||||||
|
) u
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.UserId = u.UserId AND w.IsDeleted = 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
WHERE h.WalletId = w.Id
|
||||||
|
AND h.IsDeleted = 0
|
||||||
|
AND h.RefrenceId = tx.TransactionId
|
||||||
|
);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Insert missing history
|
||||||
|
------------------------------------------------------------
|
||||||
|
;WITH Resolved AS (
|
||||||
|
SELECT
|
||||||
|
tx.TransactionId AS TxId,
|
||||||
|
t.Amount,
|
||||||
|
COALESCE(t.PaymentDate, t.Created) AS HistoryAt,
|
||||||
|
COALESCE(mp.UserId, upp.UserId) AS UserId,
|
||||||
|
COALESCE(
|
||||||
|
upp.PackageId,
|
||||||
|
NULLIF(cm.LastPackageId, 0),
|
||||||
|
NULLIF(cm.FirstPackageId, 0),
|
||||||
|
@BasePackageId
|
||||||
|
) AS PackageId,
|
||||||
|
w.Id AS WalletId,
|
||||||
|
COALESCE(w.NetworkBalance, 0) AS NetworkBalance
|
||||||
|
FROM @TargetTx tx
|
||||||
|
INNER JOIN CMS.Transactions t
|
||||||
|
ON t.Id = tx.TransactionId AND t.IsDeleted = 0 AND t.PaymentStatus = 0
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1) mp0.UserId, mp0.Id AS ManualPaymentId
|
||||||
|
FROM CMS.ManualPayments mp0
|
||||||
|
WHERE mp0.TransactionId = tx.TransactionId AND mp0.IsDeleted = 0
|
||||||
|
) mp
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP (1) upp0.UserId, upp0.PackageId
|
||||||
|
FROM CMS.UserPackagePurchases upp0
|
||||||
|
WHERE upp0.TransactionId = tx.TransactionId AND upp0.IsDeleted = 0
|
||||||
|
ORDER BY upp0.Id DESC
|
||||||
|
) upp
|
||||||
|
LEFT JOIN CMS.ClubMemberships cm
|
||||||
|
ON cm.UserId = COALESCE(mp.UserId, upp.UserId) AND cm.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.UserWallets w
|
||||||
|
ON w.UserId = COALESCE(mp.UserId, upp.UserId) AND w.IsDeleted = 0
|
||||||
|
WHERE COALESCE(mp.UserId, upp.UserId) IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
WHERE h.WalletId = w.Id
|
||||||
|
AND h.IsDeleted = 0
|
||||||
|
AND h.RefrenceId = tx.TransactionId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
INSERT INTO CMS.UserWalletHistories
|
||||||
|
(
|
||||||
|
WalletId, CurrentBalance, ChangeValue, CurrentNetworkBalance, ChangeNerworkValue,
|
||||||
|
CurrentDiscountBalance, ChangeDiscountValue, IsIncrease, RefrenceId, PackageId,
|
||||||
|
Created, CreatedBy, LastModified, LastModifiedBy, IsDeleted
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
r.WalletId,
|
||||||
|
0,
|
||||||
|
r.Amount,
|
||||||
|
r.NetworkBalance,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(ROUND(r.Amount * COALESCE(p.DiscountMultiplier, 2.0), 0) AS BIGINT),
|
||||||
|
1,
|
||||||
|
r.TxId,
|
||||||
|
r.PackageId,
|
||||||
|
r.HistoryAt,
|
||||||
|
NULL,
|
||||||
|
r.HistoryAt,
|
||||||
|
NULL,
|
||||||
|
0
|
||||||
|
FROM Resolved r
|
||||||
|
LEFT JOIN CMS.Packages p ON p.Id = r.PackageId AND p.IsDeleted = 0
|
||||||
|
WHERE r.PackageId IS NOT NULL
|
||||||
|
AND r.Amount > 0;
|
||||||
|
|
||||||
|
PRINT CONCAT('UserWalletHistory inserted: ', @@ROWCOUNT);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- Verify
|
||||||
|
------------------------------------------------------------
|
||||||
|
SELECT
|
||||||
|
tx.TransactionId,
|
||||||
|
h.Id AS WalletHistoryId,
|
||||||
|
h.ChangeValue,
|
||||||
|
h.ChangeDiscountValue,
|
||||||
|
h.RefrenceId,
|
||||||
|
h.Created AS HistoryCreated,
|
||||||
|
w.UserId
|
||||||
|
FROM @TargetTx tx
|
||||||
|
LEFT JOIN CMS.UserWalletHistories h
|
||||||
|
ON h.RefrenceId = tx.TransactionId AND h.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWallets w
|
||||||
|
ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
ORDER BY tx.TransactionId;
|
||||||
|
|
||||||
|
-- Uncomment to apply:
|
||||||
|
-- COMMIT;
|
||||||
|
ROLLBACK;
|
||||||
|
PRINT 'Rolled back (safety). Uncomment COMMIT and remove ROLLBACK to apply.';
|
||||||
@@ -0,0 +1,431 @@
|
|||||||
|
/*
|
||||||
|
DIAGNOSTIC ONLY — no data changes (read-only)
|
||||||
|
|
||||||
|
Trace WrongDaya→Manual FixUsers ledger via UserWalletHistory:
|
||||||
|
WalletHistory → CreatedBy/LastModifiedBy (mobile) → User → Wallet
|
||||||
|
→ Transaction (RefrenceId / CreatedBy-as-TxId / Tx.CreatedBy mobile)
|
||||||
|
→ UPP / ManualPayment / PurchaseMethod
|
||||||
|
|
||||||
|
Known IDs from manual CSV exports (UserWalletHistories*.csv, Transactions.csv):
|
||||||
|
WH: 102-108,122,138-142,292-295,118-121,124,201
|
||||||
|
Tx: 23-31,42-46,56-59,68,107-110
|
||||||
|
|
||||||
|
RefrenceId=0 rule (ChargeUserWallets.sql):
|
||||||
|
RefId = 'manualy' + original ChangeLogId. After id remap, WH.RefrenceId may be 0.
|
||||||
|
For Ref=0 rows, assume Tx exists ONLY if CreatedBy is numeric AND that Tx has RefId manualy%.
|
||||||
|
Do NOT infer Tx from mobile alone or from manualy{N} suffix alone.
|
||||||
|
|
||||||
|
See also: 10-Diagnose_Manualy_Transaction_Map.sql
|
||||||
|
|
||||||
|
Run on staging, review Verdict + RecommendedAction per user before any fix script.
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
-- Known WalletHistory / Transaction ids from CSV exports (for cross-check)
|
||||||
|
DECLARE @KnownWh TABLE (WalletHistoryId BIGINT PRIMARY KEY);
|
||||||
|
INSERT INTO @KnownWh (WalletHistoryId) VALUES
|
||||||
|
(102),(103),(104),(105),(106),(107),(108),(122),
|
||||||
|
(138),(139),(140),(141),(142),
|
||||||
|
(292),(293),(294),(295),
|
||||||
|
(118),(119),(120),(121),(124),(201);
|
||||||
|
|
||||||
|
DECLARE @KnownTx TABLE (TransactionId BIGINT PRIMARY KEY);
|
||||||
|
INSERT INTO @KnownTx (TransactionId) VALUES
|
||||||
|
(23),(24),(25),(26),(28),(29),(30),(31),
|
||||||
|
(42),(43),(44),(45),(46),
|
||||||
|
(56),(57),(58),(59),(68),
|
||||||
|
(107),(108),(109),(110);
|
||||||
|
|
||||||
|
DECLARE @TargetUserCount INT = (SELECT COUNT(*) FROM @FixUsers);
|
||||||
|
PRINT CONCAT('FixUsers scope: ', @TargetUserCount);
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 1) WalletHistories found BY mobile in CreatedBy / LastModifiedBy
|
||||||
|
-- (starts from WH — user request)
|
||||||
|
------------------------------------------------------------
|
||||||
|
PRINT '=== 1) WalletHistory → User via CreatedBy/LastModifiedBy mobile ===';
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
h.Id AS WalletHistoryId,
|
||||||
|
h.WalletId,
|
||||||
|
h.RefrenceId AS WhRefrenceId,
|
||||||
|
h.ChangeValue,
|
||||||
|
h.ChangeDiscountValue,
|
||||||
|
h.Created AS WhCreated,
|
||||||
|
h.CreatedBy AS WhCreatedBy,
|
||||||
|
h.LastModifiedBy AS WhLastModifiedBy,
|
||||||
|
u.Id AS UserId,
|
||||||
|
u.Mobile,
|
||||||
|
u.FirstName,
|
||||||
|
u.LastName,
|
||||||
|
u.PackagePurchaseMethod AS UserMethod,
|
||||||
|
CASE WHEN kwh.WalletHistoryId IS NOT NULL THEN 1 ELSE 0 END AS InKnownWhCsv,
|
||||||
|
CASE
|
||||||
|
WHEN u.Id IS NULL THEN N'NO_USER_FOR_MOBILE'
|
||||||
|
WHEN f.UserId IS NULL THEN N'USER_NOT_IN_FIX_SCOPE'
|
||||||
|
ELSE N'IN_FIX_SCOPE'
|
||||||
|
END AS ScopeStatus
|
||||||
|
FROM CMS.UserWalletHistories h
|
||||||
|
LEFT JOIN CMS.Users u ON u.IsDeleted = 0
|
||||||
|
AND u.Mobile IN (
|
||||||
|
NULLIF(LTRIM(RTRIM(h.CreatedBy)), N''),
|
||||||
|
NULLIF(LTRIM(RTRIM(h.LastModifiedBy)), N'')
|
||||||
|
)
|
||||||
|
LEFT JOIN @FixUsers f ON f.UserId = u.Id
|
||||||
|
LEFT JOIN @KnownWh kwh ON kwh.WalletHistoryId = h.Id
|
||||||
|
WHERE h.IsDeleted = 0
|
||||||
|
AND (
|
||||||
|
(h.CreatedBy LIKE N'09%' AND LEN(h.CreatedBy) >= 10)
|
||||||
|
OR (h.LastModifiedBy LIKE N'09%' AND LEN(h.LastModifiedBy) >= 10)
|
||||||
|
)
|
||||||
|
ORDER BY h.Id;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 2) All WalletHistories for FixUsers (via Wallet → User)
|
||||||
|
------------------------------------------------------------
|
||||||
|
PRINT '=== 2) All WalletHistory rows for FixUsers ===';
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
f.UserId,
|
||||||
|
u.Mobile,
|
||||||
|
u.FirstName,
|
||||||
|
u.LastName,
|
||||||
|
u.PackagePurchaseMethod AS UserMethod,
|
||||||
|
w.Id AS WalletId,
|
||||||
|
h.Id AS WalletHistoryId,
|
||||||
|
h.RefrenceId AS WhRefrenceId,
|
||||||
|
h.ChangeValue,
|
||||||
|
h.ChangeDiscountValue,
|
||||||
|
h.Created AS WhCreated,
|
||||||
|
h.CreatedBy AS WhCreatedBy,
|
||||||
|
h.LastModifiedBy AS WhLastModifiedBy,
|
||||||
|
CASE WHEN kwh.WalletHistoryId IS NOT NULL THEN 1 ELSE 0 END AS InKnownWhCsv,
|
||||||
|
CASE WHEN h.RefrenceId IS NULL OR h.RefrenceId = 0 THEN N'WH_NO_REF' ELSE N'WH_HAS_REF' END AS WhRefStatus
|
||||||
|
FROM @FixUsers f
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = f.UserId AND u.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.UserId = f.UserId AND w.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
|
||||||
|
LEFT JOIN @KnownWh kwh ON kwh.WalletHistoryId = h.Id
|
||||||
|
ORDER BY f.UserId, h.Id;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 3) Per-user ledger verdict (WH + Tx + UPP)
|
||||||
|
------------------------------------------------------------
|
||||||
|
PRINT '=== 3) Per-user ledger verdict ===';
|
||||||
|
|
||||||
|
;WITH UserWh AS (
|
||||||
|
SELECT
|
||||||
|
f.UserId,
|
||||||
|
COUNT(*) AS WalletHistoryCount,
|
||||||
|
SUM(CASE WHEN h.RefrenceId IS NULL OR h.RefrenceId = 0 THEN 1 ELSE 0 END) AS WhMissingRefCount,
|
||||||
|
SUM(CASE WHEN kwh.WalletHistoryId IS NOT NULL THEN 1 ELSE 0 END) AS KnownCsvWhCount,
|
||||||
|
MAX(h.Id) AS LatestWhId,
|
||||||
|
MAX(h.RefrenceId) AS LatestWhRefId
|
||||||
|
FROM @FixUsers f
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.UserId = f.UserId AND w.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
|
||||||
|
LEFT JOIN @KnownWh kwh ON kwh.WalletHistoryId = h.Id
|
||||||
|
GROUP BY f.UserId
|
||||||
|
),
|
||||||
|
UserTxViaWh AS (
|
||||||
|
SELECT
|
||||||
|
uw.UserId,
|
||||||
|
COUNT(DISTINCT t.Id) AS TxViaWhRefCount,
|
||||||
|
MAX(t.Id) AS SampleTxViaWhRef
|
||||||
|
FROM UserWh uw
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.UserId = uw.UserId AND w.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = h.RefrenceId AND t.IsDeleted = 0
|
||||||
|
GROUP BY uw.UserId
|
||||||
|
),
|
||||||
|
UserTxViaMobile AS (
|
||||||
|
SELECT
|
||||||
|
f.UserId,
|
||||||
|
COUNT(DISTINCT t.Id) AS TxViaMobileCount,
|
||||||
|
MAX(t.Id) AS SampleTxViaMobile
|
||||||
|
FROM @FixUsers f
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = f.UserId AND u.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.IsDeleted = 0
|
||||||
|
AND (t.CreatedBy = u.Mobile OR t.LastModifiedBy = u.Mobile)
|
||||||
|
GROUP BY f.UserId
|
||||||
|
),
|
||||||
|
UserTxViaCreatedByHint AS (
|
||||||
|
-- WH.CreatedBy stores Tx Id when RefrenceId=0 failed to update (UserWalletHistories2.csv).
|
||||||
|
-- Only count when hinted Tx exists AND is manualy% (may still be wrong user — verify mobile).
|
||||||
|
SELECT
|
||||||
|
f.UserId,
|
||||||
|
COUNT(DISTINCT t.Id) AS TxViaWhCreatedByHintCount,
|
||||||
|
MAX(t.Id) AS SampleTxViaHint
|
||||||
|
FROM @FixUsers f
|
||||||
|
INNER JOIN CMS.UserWallets w ON w.UserId = f.UserId AND w.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
|
||||||
|
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
|
||||||
|
WHERE (h.RefrenceId IS NULL OR h.RefrenceId = 0)
|
||||||
|
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0
|
||||||
|
GROUP BY f.UserId
|
||||||
|
),
|
||||||
|
UserTxViaManualyMobile AS (
|
||||||
|
-- Informational only — NOT used alone for Ref=0 WH linking
|
||||||
|
SELECT
|
||||||
|
f.UserId,
|
||||||
|
COUNT(DISTINCT t.Id) AS TxViaManualyMobileCount,
|
||||||
|
MAX(t.Id) AS SampleManualyTxViaMobile
|
||||||
|
FROM @FixUsers f
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = f.UserId AND u.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.IsDeleted = 0
|
||||||
|
AND t.RefId LIKE N'manualy%'
|
||||||
|
AND u.Mobile IN (
|
||||||
|
NULLIF(LTRIM(RTRIM(t.CreatedBy)), N''),
|
||||||
|
NULLIF(LTRIM(RTRIM(t.LastModifiedBy)), N'')
|
||||||
|
)
|
||||||
|
GROUP BY f.UserId
|
||||||
|
),
|
||||||
|
UserUpp AS (
|
||||||
|
SELECT
|
||||||
|
f.UserId,
|
||||||
|
COUNT(*) AS UppCount,
|
||||||
|
SUM(CASE WHEN upp.PurchaseMethod = 1 THEN 1 ELSE 0 END) AS UppDayaCount,
|
||||||
|
SUM(CASE WHEN upp.PurchaseMethod = 3 THEN 1 ELSE 0 END) AS UppManualCount,
|
||||||
|
MAX(upp.TransactionId) AS LatestUppTxId
|
||||||
|
FROM @FixUsers f
|
||||||
|
LEFT JOIN CMS.UserPackagePurchases upp
|
||||||
|
ON upp.UserId = f.UserId AND upp.IsDeleted = 0
|
||||||
|
GROUP BY f.UserId
|
||||||
|
),
|
||||||
|
UserMp AS (
|
||||||
|
SELECT
|
||||||
|
f.UserId,
|
||||||
|
COUNT(*) AS ManualPaymentCount,
|
||||||
|
MAX(mp.TransactionId) AS LatestMpTxId
|
||||||
|
FROM @FixUsers f
|
||||||
|
LEFT JOIN CMS.ManualPayments mp
|
||||||
|
ON mp.UserId = f.UserId AND mp.IsDeleted = 0
|
||||||
|
GROUP BY f.UserId
|
||||||
|
)
|
||||||
|
SELECT *
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
f.UserId,
|
||||||
|
u.Mobile,
|
||||||
|
u.FirstName,
|
||||||
|
u.LastName,
|
||||||
|
u.PackagePurchaseMethod AS UserMethod,
|
||||||
|
ISNULL(uwh.WalletHistoryCount, 0) AS WalletHistoryCount,
|
||||||
|
ISNULL(uwh.WhMissingRefCount, 0) AS WhMissingRefCount,
|
||||||
|
ISNULL(uwh.KnownCsvWhCount, 0) AS KnownCsvWhCount,
|
||||||
|
uwh.LatestWhId,
|
||||||
|
uwh.LatestWhRefId,
|
||||||
|
ISNULL(utw.TxViaWhRefCount, 0) AS TxViaWhRefCount,
|
||||||
|
utw.SampleTxViaWhRef,
|
||||||
|
ISNULL(utm.TxViaMobileCount, 0) AS TxViaMobileCount,
|
||||||
|
utm.SampleTxViaMobile,
|
||||||
|
ISNULL(uth.TxViaWhCreatedByHintCount, 0) AS TxViaWhCreatedByHintCount,
|
||||||
|
uth.SampleTxViaHint,
|
||||||
|
ISNULL(utmm.TxViaManualyMobileCount, 0) AS TxViaManualyMobileCount,
|
||||||
|
utmm.SampleManualyTxViaMobile,
|
||||||
|
ISNULL(uu.UppCount, 0) AS UppCount,
|
||||||
|
ISNULL(uu.UppDayaCount, 0) AS UppDayaCount,
|
||||||
|
uu.LatestUppTxId,
|
||||||
|
ISNULL(ump.ManualPaymentCount, 0) AS ManualPaymentCount,
|
||||||
|
ump.LatestMpTxId,
|
||||||
|
CASE
|
||||||
|
WHEN ISNULL(uwh.WalletHistoryCount, 0) = 0 THEN N'NO_WALLET_HISTORY'
|
||||||
|
WHEN ISNULL(uwh.WhMissingRefCount, 0) > 0
|
||||||
|
AND ISNULL(uth.TxViaWhCreatedByHintCount, 0) > 0 THEN N'WH_REF_BROKEN_FIX_VIA_CREATEDBY_HINT'
|
||||||
|
WHEN ISNULL(uwh.WhMissingRefCount, 0) > 0
|
||||||
|
AND ISNULL(utmm.TxViaManualyMobileCount, 0) > 0 THEN N'WH_REF_BROKEN_MANUALY_TX_BY_MOBILE_REVIEW'
|
||||||
|
WHEN ISNULL(uwh.WhMissingRefCount, 0) > 0 THEN N'WH_REF_BROKEN_NO_TX_HINT'
|
||||||
|
WHEN ISNULL(utw.TxViaWhRefCount, 0) > 0
|
||||||
|
AND ISNULL(uu.UppCount, 0) = 0 THEN N'HAS_WH+TX_NEED_UPP'
|
||||||
|
WHEN ISNULL(utw.TxViaWhRefCount, 0) > 0
|
||||||
|
AND ISNULL(uu.UppDayaCount, 0) > 0 THEN N'HAS_WH+TX_UPP_DAYA_FIX_METHOD'
|
||||||
|
WHEN ISNULL(utw.TxViaWhRefCount, 0) > 0 THEN N'OK_WH+TX'
|
||||||
|
WHEN ISNULL(utm.TxViaMobileCount, 0) > 0 THEN N'TX_BY_MOBILE_WH_LINK_UNCLEAR'
|
||||||
|
ELSE N'NEEDS_MANUAL_REVIEW'
|
||||||
|
END AS Verdict,
|
||||||
|
CASE
|
||||||
|
WHEN ISNULL(uwh.WalletHistoryCount, 0) = 0 THEN N'Create WH+Tx+UPP (or find hidden WH by mobile)'
|
||||||
|
WHEN ISNULL(uwh.WhMissingRefCount, 0) > 0
|
||||||
|
AND ISNULL(uth.TxViaWhCreatedByHintCount, 0) > 0 THEN N'Link WH.RefrenceId to manualy Tx from CreatedBy hint only'
|
||||||
|
WHEN ISNULL(uwh.WhMissingRefCount, 0) > 0
|
||||||
|
AND ISNULL(utmm.TxViaManualyMobileCount, 0) > 0 THEN N'manualy Tx exists by mobile but WH hint missing — manual review'
|
||||||
|
WHEN ISNULL(uwh.WhMissingRefCount, 0) > 0 THEN N'No Tx hint — do NOT assume Tx exists; review charge log'
|
||||||
|
WHEN ISNULL(utw.TxViaWhRefCount, 0) > 0 AND ISNULL(uu.UppCount, 0) = 0 THEN N'Create UPP Manual(3) reusing Tx'
|
||||||
|
WHEN ISNULL(uu.UppDayaCount, 0) > 0 OR u.PackagePurchaseMethod = 1 THEN N'Fix PurchaseMethod 1→3 only'
|
||||||
|
ELSE N'Likely OK — verify PurchaseMethod'
|
||||||
|
END AS RecommendedAction
|
||||||
|
FROM @FixUsers f
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = f.UserId AND u.IsDeleted = 0
|
||||||
|
LEFT JOIN UserWh uwh ON uwh.UserId = f.UserId
|
||||||
|
LEFT JOIN UserTxViaWh utw ON utw.UserId = f.UserId
|
||||||
|
LEFT JOIN UserTxViaMobile utm ON utm.UserId = f.UserId
|
||||||
|
LEFT JOIN UserTxViaCreatedByHint uth ON uth.UserId = f.UserId
|
||||||
|
LEFT JOIN UserTxViaManualyMobile utmm ON utmm.UserId = f.UserId
|
||||||
|
LEFT JOIN UserUpp uu ON uu.UserId = f.UserId
|
||||||
|
LEFT JOIN UserMp ump ON ump.UserId = f.UserId
|
||||||
|
) q
|
||||||
|
ORDER BY
|
||||||
|
CASE q.Verdict
|
||||||
|
WHEN N'OK_WH+TX' THEN 9
|
||||||
|
WHEN N'HAS_WH+TX_UPP_DAYA_FIX_METHOD' THEN 8
|
||||||
|
ELSE 1
|
||||||
|
END,
|
||||||
|
q.UserId;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 4) Summary counts by Verdict
|
||||||
|
------------------------------------------------------------
|
||||||
|
PRINT '=== 4) Summary by Verdict ===';
|
||||||
|
|
||||||
|
;WITH PerUser AS (
|
||||||
|
SELECT
|
||||||
|
f.UserId,
|
||||||
|
u.PackagePurchaseMethod AS UserMethod,
|
||||||
|
ISNULL(uwh.Cnt, 0) AS WhCnt,
|
||||||
|
ISNULL(uwh.BadRefCnt, 0) AS WhBadRefCnt,
|
||||||
|
ISNULL(utw.Cnt, 0) AS TxViaWhCnt,
|
||||||
|
ISNULL(utm.Cnt, 0) AS TxViaMobileCnt,
|
||||||
|
ISNULL(uth.Cnt, 0) AS TxViaHintCnt,
|
||||||
|
ISNULL(uu.Cnt, 0) AS UppCnt
|
||||||
|
FROM @FixUsers f
|
||||||
|
INNER JOIN CMS.Users u ON u.Id = f.UserId AND u.IsDeleted = 0
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT COUNT(*) AS Cnt,
|
||||||
|
SUM(CASE WHEN h.RefrenceId IS NULL OR h.RefrenceId = 0 THEN 1 ELSE 0 END) AS BadRefCnt
|
||||||
|
FROM CMS.UserWallets w
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
|
||||||
|
WHERE w.UserId = f.UserId AND w.IsDeleted = 0
|
||||||
|
) uwh
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT COUNT(DISTINCT t.Id) AS Cnt
|
||||||
|
FROM CMS.UserWallets w
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = h.RefrenceId AND t.IsDeleted = 0
|
||||||
|
WHERE w.UserId = f.UserId AND w.IsDeleted = 0
|
||||||
|
) utw
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT COUNT(DISTINCT t.Id) AS Cnt
|
||||||
|
FROM CMS.Transactions t
|
||||||
|
WHERE t.IsDeleted = 0
|
||||||
|
AND (t.CreatedBy = u.Mobile OR t.LastModifiedBy = u.Mobile)
|
||||||
|
) utm
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT COUNT(DISTINCT t.Id) AS Cnt
|
||||||
|
FROM CMS.UserWallets w
|
||||||
|
INNER JOIN CMS.UserWalletHistories h ON h.WalletId = w.Id AND h.IsDeleted = 0
|
||||||
|
INNER JOIN CMS.Transactions t ON t.Id = TRY_CAST(h.CreatedBy AS BIGINT)
|
||||||
|
AND t.IsDeleted = 0 AND t.RefId LIKE N'manualy%'
|
||||||
|
WHERE w.UserId = f.UserId AND w.IsDeleted = 0
|
||||||
|
AND (h.RefrenceId IS NULL OR h.RefrenceId = 0)
|
||||||
|
AND TRY_CAST(h.CreatedBy AS BIGINT) > 0
|
||||||
|
) uth
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT COUNT(*) AS Cnt
|
||||||
|
FROM CMS.UserPackagePurchases upp
|
||||||
|
WHERE upp.UserId = f.UserId AND upp.IsDeleted = 0
|
||||||
|
) uu
|
||||||
|
),
|
||||||
|
Verdicts AS (
|
||||||
|
SELECT
|
||||||
|
UserId,
|
||||||
|
CASE
|
||||||
|
WHEN WhCnt = 0 THEN N'NO_WALLET_HISTORY'
|
||||||
|
WHEN WhBadRefCnt > 0 AND TxViaHintCnt > 0 THEN N'WH_REF_BROKEN_FIX_VIA_CREATEDBY_HINT'
|
||||||
|
WHEN WhBadRefCnt > 0 THEN N'WH_REF_BROKEN_NO_TX_HINT'
|
||||||
|
WHEN TxViaWhCnt > 0 AND UppCnt = 0 THEN N'HAS_WH+TX_NEED_UPP'
|
||||||
|
WHEN TxViaWhCnt > 0 THEN N'OK_WH+TX'
|
||||||
|
WHEN TxViaMobileCnt > 0 THEN N'TX_BY_MOBILE_WH_LINK_UNCLEAR'
|
||||||
|
ELSE N'NEEDS_MANUAL_REVIEW'
|
||||||
|
END AS Verdict
|
||||||
|
FROM PerUser
|
||||||
|
)
|
||||||
|
SELECT Verdict, COUNT(*) AS UserCount
|
||||||
|
FROM Verdicts
|
||||||
|
GROUP BY Verdict
|
||||||
|
ORDER BY UserCount DESC;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 5) Known CSV Tx ids — exist in DB? linked to WH/UPP?
|
||||||
|
------------------------------------------------------------
|
||||||
|
PRINT '=== 5) Known Transaction IDs from CSV ===';
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
kt.TransactionId,
|
||||||
|
CASE WHEN t.Id IS NULL THEN N'MISSING_IN_DB' ELSE N'EXISTS' END AS TxStatus,
|
||||||
|
t.Amount,
|
||||||
|
t.Description,
|
||||||
|
t.RefId,
|
||||||
|
t.CreatedBy AS TxCreatedBy,
|
||||||
|
t.LastModifiedBy AS TxLastModifiedBy,
|
||||||
|
u.Id AS UserByMobile,
|
||||||
|
u.Mobile,
|
||||||
|
h.Id AS LinkedWalletHistoryId,
|
||||||
|
h.WalletId,
|
||||||
|
h.RefrenceId AS WhRefrenceId,
|
||||||
|
upp.UserId AS UppUserId,
|
||||||
|
upp.PurchaseMethod AS UppMethod
|
||||||
|
FROM @KnownTx kt
|
||||||
|
LEFT JOIN CMS.Transactions t ON t.Id = kt.TransactionId
|
||||||
|
LEFT JOIN CMS.Users u ON u.IsDeleted = 0
|
||||||
|
AND u.Mobile IN (NULLIF(t.CreatedBy, N''), NULLIF(t.LastModifiedBy, N''))
|
||||||
|
LEFT JOIN CMS.UserWalletHistories h ON h.IsDeleted = 0 AND h.RefrenceId = kt.TransactionId
|
||||||
|
LEFT JOIN CMS.UserPackagePurchases upp ON upp.IsDeleted = 0 AND upp.TransactionId = kt.TransactionId
|
||||||
|
ORDER BY kt.TransactionId;
|
||||||
|
|
||||||
|
------------------------------------------------------------
|
||||||
|
-- 6) Known CSV WH ids — RefrenceId vs CreatedBy hint vs mobile
|
||||||
|
------------------------------------------------------------
|
||||||
|
PRINT '=== 6) Known WalletHistory IDs from CSV ===';
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
kw.WalletHistoryId,
|
||||||
|
h.WalletId,
|
||||||
|
h.RefrenceId AS WhRefrenceId,
|
||||||
|
h.ChangeValue,
|
||||||
|
h.CreatedBy AS WhCreatedBy,
|
||||||
|
h.LastModifiedBy AS WhLastModifiedBy,
|
||||||
|
w.UserId AS WalletUserId,
|
||||||
|
u.Mobile AS WalletUserMobile,
|
||||||
|
u_by_mobile.Id AS UserByWhMobile,
|
||||||
|
u_by_mobile.Mobile AS MatchedMobile,
|
||||||
|
t_ref.Id AS TxByRefrenceId,
|
||||||
|
t_ref.Description AS TxByRefDescription,
|
||||||
|
t_hint.Id AS TxByCreatedByHint,
|
||||||
|
t_hint.RefId AS TxByHintRefId,
|
||||||
|
t_hint.Description AS TxByHintDescription,
|
||||||
|
TRY_CAST(
|
||||||
|
SUBSTRING(t_hint.RefId, LEN(N'manualy') + 1, 20)
|
||||||
|
AS BIGINT
|
||||||
|
) AS ManualyOriginalChangeLogId,
|
||||||
|
CASE
|
||||||
|
WHEN h.Id IS NULL THEN N'WH_MISSING_IN_DB'
|
||||||
|
WHEN t_ref.Id IS NOT NULL AND t_ref.RefId LIKE N'manualy%' THEN N'WH_LINKED_TO_MANUALY_TX'
|
||||||
|
WHEN t_ref.Id IS NOT NULL THEN N'WH_LINKED_TO_OTHER_TX'
|
||||||
|
WHEN t_hint.Id IS NOT NULL AND t_hint.RefId LIKE N'manualy%' THEN N'WH_REF_BROKEN_USE_CREATEDBY_HINT'
|
||||||
|
WHEN TRY_CAST(h.CreatedBy AS BIGINT) > 0 AND t_hint.Id IS NULL THEN N'WH_HINT_TX_MISSING'
|
||||||
|
WHEN u_by_mobile.Id IS NOT NULL THEN N'WH_MOBILE_ONLY_NO_AUTO_LINK'
|
||||||
|
ELSE N'WH_ORPHAN_REVIEW'
|
||||||
|
END AS WhLinkStatus
|
||||||
|
FROM @KnownWh kw
|
||||||
|
LEFT JOIN CMS.UserWalletHistories h ON h.Id = kw.WalletHistoryId AND h.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.UserWallets w ON w.Id = h.WalletId AND w.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Users u ON u.Id = w.UserId AND u.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Users u_by_mobile ON u_by_mobile.IsDeleted = 0
|
||||||
|
AND u_by_mobile.Mobile IN (
|
||||||
|
NULLIF(LTRIM(RTRIM(h.CreatedBy)), N''),
|
||||||
|
NULLIF(LTRIM(RTRIM(h.LastModifiedBy)), N'')
|
||||||
|
)
|
||||||
|
LEFT JOIN CMS.Transactions t_ref ON t_ref.Id = h.RefrenceId AND t_ref.IsDeleted = 0
|
||||||
|
LEFT JOIN CMS.Transactions t_hint ON t_hint.Id = TRY_CAST(h.CreatedBy AS BIGINT) AND t_hint.IsDeleted = 0
|
||||||
|
ORDER BY kw.WalletHistoryId;
|
||||||
|
|
||||||
|
PRINT 'Done (diagnostic only — no changes).';
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
"WalletHistoryId","WalletId","WhRefrenceId","ChangeValue","ChangeDiscountValue","WhCreated","WhCreatedBy","WhLastModifiedBy","UserId","Mobile","FirstName","LastName","UserMethod","InKnownWhCsv","ScopeStatus"
|
||||||
|
"118","34","0","1","0","3/12/2025 09:49:45.92","56","09015793338","71","09015793338","حمیدرضا","مرجانی","3","1","IN_FIX_SCOPE"
|
||||||
|
"119","32","0","1","0","19/11/2025 01:24:24","58","09217672935","69","09217672935","مهدی ","گیوی ","3","1","IN_FIX_SCOPE"
|
||||||
|
"120","37","0","1","0","19/11/2025 01:24:24","59","09101692049","74","09101692049","سجاد ","اداوی","3","1","IN_FIX_SCOPE"
|
||||||
|
"121","12","0","1","0","19/12/2025 22:00:40.276666","0","09387342688","2","09387342688","رباب","مرجانی","3","1","IN_FIX_SCOPE"
|
||||||
|
"124","35","0","1","0","19/11/2025 01:24:24","57","09961354257",,,,,,"1","NO_USER_FOR_MOBILE"
|
||||||
|
"125","42","53","56000000","112000000","3/12/2025 09:49:45.92","09172606097","09172606097","88","09172606097","کریم","رعیت پیشه","1","0","IN_FIX_SCOPE"
|
||||||
|
"126","71","54","56000000","112000000","3/12/2025 09:49:45.92","09199105580","09199105580","123","09199105580","ناصر ","کریمی پیروز ","1","0","USER_NOT_IN_FIX_SCOPE"
|
||||||
|
"127","43","55","56000000","112000000","3/12/2025 09:49:45.92","09174337813","09174337813","90","09174337813","علیرضا ","پولادی","1","0","IN_FIX_SCOPE"
|
||||||
|
"131","62","35","56000000","112000000","3/12/2025 09:49:45.92","09397939699","09397939699","114","09397939699","اکرم","دهقانی","1","0","USER_NOT_IN_FIX_SCOPE"
|
||||||
|
"132","50","36","56000000","112000000","3/12/2025 09:49:45.92","09367537393","09367537393","101","09367537393","مبینا","مالمیر","1","0","USER_NOT_IN_FIX_SCOPE"
|
||||||
|
"133","66","37","56000000","112000000","3/12/2025 09:49:45.92","09926262237","09926262237","118","09926262237","مهدی ","محیط آبادی","1","0","USER_NOT_IN_FIX_SCOPE"
|
||||||
|
"134","73","38","56000000","112000000","3/12/2025 09:49:45.92","09126302293","09126302293","125","09126302293","محمود","دادگرکریمی","1","0","IN_FIX_SCOPE"
|
||||||
|
"135","70","39","56000000","112000000","3/12/2025 09:49:45.92","09107033016","09107033016","122","09107033016","ابوالقاسم","عابدی","1","0","USER_NOT_IN_FIX_SCOPE"
|
||||||
|
"136","44","40","56000000","112000000","3/12/2025 09:49:45.92","09390081668","09390081668","91","09390081668","رحیم ","رعیت پیشه","1","0","IN_FIX_SCOPE"
|
||||||
|
"137","69","41","56000000","112000000","3/12/2025 09:49:45.92","09332726075","09332726075","121","09332726075","محمد","جعفری","1","0","IN_FIX_SCOPE"
|
||||||
|
"138","64","42","56000000","0","3/12/2025 09:49:45.92","09014638673","09014638673","116","09014638673","سالار","رضائی","3","1","IN_FIX_SCOPE"
|
||||||
|
"139","63","43","56000000","0","3/12/2025 09:49:45.92","09056365323","09056365323","115","09056365323","حامد","دهقانی","3","1","IN_FIX_SCOPE"
|
||||||
|
"140","67","44","56000000","0","3/12/2025 09:49:45.92","09126092994","09126092994","119","09126092994","علیرضا ","کریمی پیروز ","3","1","IN_FIX_SCOPE"
|
||||||
|
"141","61","45","56000000","0","3/12/2025 09:49:45.92","09177325630","09177325630","113","09177325630","کاوس","بیگ اینالو","3","1","IN_FIX_SCOPE"
|
||||||
|
"142","51","46","56000000","0","3/12/2025 09:49:45.92","09384366040","09384366040","103","09384366040","علی ","دلال زاده","3","1","IN_FIX_SCOPE"
|
||||||
|
"144","72","61","56000000","112000000","8/12/2025 12:30:53.536666","09120389056","09120389056","124","09120389056","محمدرضا ","باغجری","1","0","USER_NOT_IN_FIX_SCOPE"
|
||||||
|
"145","74","62","56000000","112000000","8/12/2025 12:30:53.536666","09191024013","09191024013","126","09191024013"," امیرعباس","میرزایی","1","0","USER_NOT_IN_FIX_SCOPE"
|
||||||
|
"146","86","63","56000000","112000000","8/12/2025 12:30:53.536666","09125032770","09125032770","138","09125032770","سیما","اکبرزاده","1","0","USER_NOT_IN_FIX_SCOPE"
|
||||||
|
"147","87","64","56000000","112000000","8/12/2025 12:30:53.536666","09102618416","09102618416","139","09102618416","مسعود","توسلیان","1","0","USER_NOT_IN_FIX_SCOPE"
|
||||||
|
"148","88","65","56000000","112000000","8/12/2025 12:30:53.536666","09121444951","09121444951","140","09121444951","سید سعید ","بزرگ نیا حسینی","1","0","USER_NOT_IN_FIX_SCOPE"
|
||||||
|
"149","89","66","56000000","112000000","8/12/2025 12:30:53.536666","09123233123","09123233123","141","09123233123","بهزاد","بابااصفهانی","1","0","USER_NOT_IN_FIX_SCOPE"
|
||||||
|
"174","91","87","56000000","0","19/12/2025 18:11:34.32","09358325024","09358325024","143","09358325024","مهدیه","زیبائی","1","0","USER_NOT_IN_FIX_SCOPE"
|
||||||
|
"352","130","146","56000000","112000000","22/2/2026 16:02:19.2219367","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"363","133","170","5600000","11200000","9/3/2026 10:34:00.2814919","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"365","136","173","5600000","11200000","15/3/2026 10:54:59.2500419","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"40387","39","40193","5600000","11200000","4/5/2026 21:55:45.9173742","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"40394","150","40202","5600000","11200000","7/5/2026 15:57:11.887558","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"40469","163","40269","56000000","112000000","25/5/2026 21:01:00.302016","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"40516","170","40293","5600000","11200000","1/6/2026 19:30:29.811968","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"40517","168","40294","5600000","11200000","1/6/2026 19:30:50.6897351","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"40524","172","40301","5600000","11200000","4/6/2026 15:21:57.0889467","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"40525","176","40302","5600000","11200000","5/6/2026 15:12:14.4460401","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"40526","174","40303","5600000","11200000","5/6/2026 15:12:31.6951342","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"40527","175","40304","5600000","11200000","5/6/2026 15:12:43.4902779","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"40552","183","40326","5600000","11200000","9/6/2026 20:31:28.0962845","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"40553","188","40327","5600000","11200000","9/6/2026 20:32:03.6166647","09199877503","09199877503","162","09199877503","مسعود","آفرین مقدم","3","0","IN_FIX_SCOPE"
|
||||||
|
"2","09387342688","رباب","مرجانی","3","12","121","0","1","0","19/12/2025 22:00:40.276666","0","09387342688","1","WH_NO_REF"
|
||||||
|
"2","09387342688","رباب","مرجانی","3","12","327","92","0","0","31/1/2026 10:20:55.9566667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"2","09387342688","رباب","مرجانی","3","12","40654","154","0","0","27/6/2026 11:52:47.0866667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","185","106","0","0","19/12/2025 21:05:21.956666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","198","113","0","0","19/12/2025 22:00:40.276666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","202","9","0","0","20/12/2025 09:56:37.616666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","206","13","0","0","20/12/2025 10:30:49.186666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","212","19","0","0","20/12/2025 10:31:12.52","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","223","30","0","0","20/12/2025 10:31:24.903333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","230","37","0","0","20/12/2025 10:31:48.963333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","233","40","0","0","20/12/2025 10:38:43.333333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","239","46","0","0","20/12/2025 10:39:58.833333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","250","57","0","0","20/12/2025 10:41:04.043333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","257","64","0","0","20/12/2025 10:42:03.116666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","286","78","0","0","28/12/2025 18:05:47.7766667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","297","84","0","0","3/1/2026 09:05:11.38","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","313","88","0","0","15/1/2026 15:51:15.5433333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","328","93","0","0","31/1/2026 10:20:55.9566667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","40380","99","0","0","4/5/2026 21:49:21.8933333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","40384","103","0","0","4/5/2026 21:51:42.8666667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","40454","115","0","0","23/5/2026 21:18:37.59","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","40484","127","0","0","29/5/2026 18:46:36.1866667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","40530","132","0","0","6/6/2026 13:35:49.2833333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","40557","138","0","0","13/6/2026 13:03:08.3066667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","40592","147","0","0","20/6/2026 06:08:53.4","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","40655","155","0","0","27/6/2026 11:52:47.0866667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","90716","20177","0","0","25/7/2026 06:58:12.6766667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","110825","30185","0","0","7/8/2026 22:33:52.54","SP","SP","0","WH_HAS_REF"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","13","120888","30203","0","0","15/8/2026 05:48:36.46","SP","SP","0","WH_HAS_REF"
|
||||||
|
"8","09126612405","مهدی","صیفی","3","14","40385","104","0","0","4/5/2026 21:51:42.8666667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"8","09126612405","مهدی","صیفی","3","14","40656","156","0","0","27/6/2026 11:52:47.0866667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"8","09126612405","مهدی","صیفی","3","14","90717","20178","0","0","25/7/2026 06:58:12.6766667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"9","09387095611","مهدی","مرجانی","3","15","186","107","0","0","19/12/2025 21:05:21.956666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"9","09387095611","مهدی","مرجانی","3","15","213","20","0","0","20/12/2025 10:31:12.52","SP","SP","0","WH_HAS_REF"
|
||||||
|
"9","09387095611","مهدی","مرجانی","3","15","224","31","0","0","20/12/2025 10:31:24.903333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"9","09387095611","مهدی","مرجانی","3","15","240","47","0","0","20/12/2025 10:39:58.833333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"9","09387095611","مهدی","مرجانی","3","15","251","58","0","0","20/12/2025 10:41:04.043333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"9","09387095611","مهدی","مرجانی","3","15","287","79","0","0","28/12/2025 18:05:47.7766667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"9","09387095611","مهدی","مرجانی","3","15","329","94","0","0","31/1/2026 10:20:55.9566667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"9","09387095611","مهدی","مرجانی","3","15","40386","105","0","0","4/5/2026 21:51:42.8666667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"9","09387095611","مهدی","مرجانی","3","15","40398","106","0","0","10/5/2026 20:32:00.55","SP","SP","0","WH_HAS_REF"
|
||||||
|
"9","09387095611","مهدی","مرجانی","3","15","40455","116","0","0","23/5/2026 21:18:37.59","SP","SP","0","WH_HAS_REF"
|
||||||
|
"9","09387095611","مهدی","مرجانی","3","15","40485","128","0","0","29/5/2026 18:46:36.1866667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"40","09124287588","مهدی","مرجانی","3","20","288","80","0","0","28/12/2025 18:05:47.7766667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"40","09124287588","مهدی","مرجانی","3","20","330","95","0","0","31/1/2026 10:20:55.9566667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"40","09124287588","مهدی","مرجانی","3","20","40482","125","0","0","29/5/2026 18:46:36.0866667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"40","09124287588","مهدی","مرجانی","3","20","40531","133","0","0","6/6/2026 13:35:49.2833333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"40","09124287588","مهدی","مرجانی","3","20","40558","139","0","0","13/6/2026 13:03:08.3066667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"40","09124287588","مهدی","مرجانی","3","20","40657","157","0","0","27/6/2026 11:52:47.0866667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"40","09124287588","مهدی","مرجانی","3","20","50671","164","0","0","5/7/2026 09:38:54.27","SP","SP","0","WH_HAS_REF"
|
||||||
|
"40","09124287588","مهدی","مرجانی","3","20","70686","20164","0","0","18/7/2026 06:15:56.3666667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"40","09124287588","مهدی","مرجانی","3","20","100771","30177","0","0","3/8/2026 11:26:51.4","SP","SP","0","WH_HAS_REF"
|
||||||
|
"40","09124287588","مهدی","مرجانی","3","20","110826","30186","0","0","7/8/2026 22:33:52.54","SP","SP","0","WH_HAS_REF"
|
||||||
|
"40","09124287588","مهدی","مرجانی","3","20","120889","30204","0","0","15/8/2026 05:48:36.46","SP","SP","0","WH_HAS_REF"
|
||||||
|
"40","09124287588","مهدی","مرجانی","3","20","140905","40203","0","0","22/8/2026 09:15:24.6","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","107","29","56000000","0","19/11/2025 01:24:24",,,"1","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","176","97","0","0","19/12/2025 20:47:49.22","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","187","108","0","0","19/12/2025 21:05:21.956666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","203","10","0","0","20/12/2025 09:56:37.616666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","207","14","0","0","20/12/2025 10:30:49.186666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","214","21","0","0","20/12/2025 10:31:12.52","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","225","32","0","0","20/12/2025 10:31:24.903333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","234","41","0","0","20/12/2025 10:38:43.333333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","241","48","0","0","20/12/2025 10:39:58.833333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","252","59","0","0","20/12/2025 10:41:04.043333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","289","81","0","0","28/12/2025 18:05:47.7766667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","298","85","0","0","3/1/2026 09:05:11.38","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","314","89","0","0","15/1/2026 15:51:15.5433333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","40421","40226","-8030000","0","14/5/2026 05:29:06.5412764","43","43","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","40422","40227","-7810000","0","14/5/2026 05:32:17.6846523","43","43","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","40423","40229","-3121800","0","14/5/2026 05:39:57.0293431","43","43","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","40424","40230","-3497440","0","14/5/2026 06:43:14.495774","43","43","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","60673","10164","0","0","11/7/2026 06:48:37.5","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","70687","20165","0","0","18/7/2026 06:15:56.3666667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","100772","30178","0","0","3/8/2026 11:26:51.4","SP","SP","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","110810","110572","-2084900","0","6/8/2026 09:52:06.8511076","43","43","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","110811","110573","0","-98100","6/8/2026 09:55:13.7439233","43","43","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","110812","110573","0","-98100","6/8/2026 10:14:12.789987","43","43","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","110817","110573","0","-98100","6/8/2026 13:34:54.8426154","50282","50282","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","110846","110605","-913000","0","8/8/2026 15:21:48.1642179","43","43","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","110847","110606","-2702670","0","8/8/2026 15:29:25.4361709","43","43","0","WH_HAS_REF"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","22","140906","40204","0","0","22/8/2026 09:15:24.6","SP","SP","0","WH_HAS_REF"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","23","108","30","56000000","0","19/11/2025 01:24:24",,,"1","WH_HAS_REF"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","23","153","51","-24690000","0","13/12/2025 01:20:08.176666","45","45","0","WH_HAS_REF"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","23","177","98","0","0","19/12/2025 20:47:49.22","SP","SP","0","WH_HAS_REF"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","23","188","109","0","0","19/12/2025 21:05:21.956666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","23","215","22","0","0","20/12/2025 10:31:12.52","SP","SP","0","WH_HAS_REF"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","23","226","33","0","0","20/12/2025 10:31:24.903333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","23","242","49","0","0","20/12/2025 10:39:58.833333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","23","253","60","0","0","20/12/2025 10:41:04.043333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","23","264","97","-13449150","0","23/12/2025 17:41:19.995818","45","45","0","WH_HAS_REF"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","23","40370","40179","-5222999","0","22/4/2026 09:39:04.6856636","45","45","0","WH_HAS_REF"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","23","40411","40214","-2021987","0","11/5/2026 22:15:08.7121357","45","45","0","WH_HAS_REF"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","23","100755","100519","-2247800","0","30/7/2026 11:32:50.7987614","45","45","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","106","28","56000000","0","19/11/2025 01:24:24",,,"1","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","171","84","-5353400","0","19/12/2025 10:08:54.173333","47","47","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","172","85","-1757500","0","19/12/2025 10:11:34.32","47","47","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","178","99","0","0","19/12/2025 20:47:49.22","SP","SP","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","189","110","0","0","19/12/2025 21:05:21.956666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","204","11","0","0","20/12/2025 09:56:37.616666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","208","15","0","0","20/12/2025 10:30:49.186666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","216","23","0","0","20/12/2025 10:31:12.52","SP","SP","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","227","34","0","0","20/12/2025 10:31:24.903333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","235","42","0","0","20/12/2025 10:38:43.333333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","243","50","0","0","20/12/2025 10:39:58.833333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","254","61","0","0","20/12/2025 10:41:04.043333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","271","104","-2857800","0","26/12/2025 06:35:54.4469592","47","47","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","272","105","-924000","0","26/12/2025 06:38:49.1560479","47","47","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","307","116","-774400","0","5/1/2026 09:41:18.2212775","47","47","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","358","163","-8030000","0","24/2/2026 12:31:23.560765","47","47","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","40397","40205","-3380600","0","10/5/2026 13:59:49.4056935","47","47","0","WH_HAS_REF"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","24","40500","40275","-165000","0","31/5/2026 08:38:01.8954219","47","47","0","WH_HAS_REF"
|
||||||
|
"52","09178327020","حمیدرضا ","اسمعیلی ","3","26","122","31","56000000","0","27/11/2025 01:24:24",,,"1","WH_HAS_REF"
|
||||||
|
"58","09362102420","وحید","حق گو","1","8","115","7","56000000","112000000","19/11/2025 01:24:24",,,"0","WH_HAS_REF"
|
||||||
|
"69","09217672935","مهدی ","گیوی ","3","32","119","0","1","0","19/11/2025 01:24:24","58","09217672935","1","WH_NO_REF"
|
||||||
|
"69","09217672935","مهدی ","گیوی ","3","32","209","0","0","0","20/12/2025 10:30:49.186666","16","SP","0","WH_NO_REF"
|
||||||
|
"69","09217672935","مهدی ","گیوی ","3","32","236","0","0","0","20/12/2025 10:38:43.333333","43","SP","0","WH_NO_REF"
|
||||||
|
"69","09217672935","مهدی ","گیوی ","3","32","40430","111","0","0","16/5/2026 18:45:37.26","SP","SP","0","WH_HAS_REF"
|
||||||
|
"71","09015793338","حمیدرضا","مرجانی","3","34","118","0","1","0","3/12/2025 09:49:45.92","56","09015793338","1","WH_NO_REF"
|
||||||
|
"71","09015793338","حمیدرضا","مرجانی","3","34","218","0","0","0","20/12/2025 10:31:12.52","25","SP","0","WH_NO_REF"
|
||||||
|
"71","09015793338","حمیدرضا","مرجانی","3","34","245","0","0","0","20/12/2025 10:39:58.833333","52","SP","0","WH_NO_REF"
|
||||||
|
"71","09015793338","حمیدرضا","مرجانی","3","34","40399","107","0","0","10/5/2026 20:32:00.55","SP","SP","0","WH_HAS_REF"
|
||||||
|
"71","09015793338","حمیدرضا","مرجانی","3","34","40555","40329","-165000","0","12/6/2026 09:48:16.6573626","71","71","0","WH_HAS_REF"
|
||||||
|
"71","09015793338","حمیدرضا","مرجانی","3","34","110827","30187","0","0","7/8/2026 22:33:52.54","SP","SP","0","WH_HAS_REF"
|
||||||
|
"72","09124678697","مژگان","گیوی","3","35","124","0","1","0","19/11/2025 01:24:24","57","09961354257","1","WH_NO_REF"
|
||||||
|
"74","09101692049","سجاد ","اداوی","3","37","120","0","1","0","19/11/2025 01:24:24","59","09101692049","1","WH_NO_REF"
|
||||||
|
"74","09101692049","سجاد ","اداوی","3","37","219","0","0","0","20/12/2025 10:31:12.52","26","SP","0","WH_NO_REF"
|
||||||
|
"74","09101692049","سجاد ","اداوی","3","37","246","0","0","0","20/12/2025 10:39:58.833333","53","SP","0","WH_NO_REF"
|
||||||
|
"74","09101692049","سجاد ","اداوی","3","37","40381","100","0","0","4/5/2026 21:49:21.8933333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"74","09101692049","سجاد ","اداوی","3","37","40382","101","0","0","4/5/2026 21:51:01.7466667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"74","09101692049","سجاد ","اداوی","3","37","40383","102","0","0","4/5/2026 21:51:20.3866667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"74","09101692049","سجاد ","اداوی","3","37","40431","112","0","0","16/5/2026 18:45:37.26","SP","SP","0","WH_HAS_REF"
|
||||||
|
"74","09101692049","سجاد ","اداوی","3","37","40532","134","0","0","6/6/2026 13:35:49.2833333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","102","23","56000000","0","19/11/2025 01:24:24",,,"1","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","199","114","0","0","19/12/2025 22:00:40.276666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","205","12","0","0","20/12/2025 09:56:37.616666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","210","17","0","0","20/12/2025 10:30:49.186666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","231","38","0","0","20/12/2025 10:31:48.963333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","237","44","0","0","20/12/2025 10:38:43.333333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","258","65","0","0","20/12/2025 10:42:03.116666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","299","86","0","0","3/1/2026 09:05:11.38","SP","SP","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","340","134","-7810000","0","12/2/2026 18:59:26.4359166","75","75","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","341","135","-13585000","0","13/2/2026 11:43:52.8147951","75","75","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","342","136","-7810000","0","13/2/2026 18:03:25.0356764","75","75","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","344","138","-8030000","0","15/2/2026 14:16:41.8568639","75","75","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","346","140","-4600200","0","16/2/2026 09:15:32.9394144","75","75","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","40369","40178","-7810000","0","21/4/2026 15:25:58.0230953","75","75","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","40400","108","0","0","10/5/2026 20:32:00.55","SP","SP","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","40406","40209","-825200","0","11/5/2026 13:43:42.9497941","75","75","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","40407","40210","-1865974","0","11/5/2026 13:48:07.5718054","75","75","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","40518","40295","-3630000","0","2/6/2026 11:47:15.7293433","75","75","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","40559","140","0","0","13/6/2026 13:03:08.3066667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","38","120890","30205","0","0","15/8/2026 05:48:36.46","SP","SP","0","WH_HAS_REF"
|
||||||
|
"87","09020569440","امیررضا","محمدی","1","9","116","8","56000000","112000000","19/11/2025 01:24:24",,,"0","WH_HAS_REF"
|
||||||
|
"87","09020569440","امیررضا","محمدی","1","9","182","103","0","0","19/12/2025 20:47:49.22","SP","SP","0","WH_HAS_REF"
|
||||||
|
"87","09020569440","امیررضا","محمدی","1","9","220","27","0","0","20/12/2025 10:31:12.52","SP","SP","0","WH_HAS_REF"
|
||||||
|
"87","09020569440","امیررضا","محمدی","1","9","247","54","0","0","20/12/2025 10:39:58.833333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"88","09172606097","کریم","رعیت پیشه","1","42","125","53","56000000","112000000","3/12/2025 09:49:45.92","09172606097","09172606097","0","WH_HAS_REF"
|
||||||
|
"88","09172606097","کریم","رعیت پیشه","1","42","183","104","0","0","19/12/2025 20:47:49.22","SP","SP","0","WH_HAS_REF"
|
||||||
|
"88","09172606097","کریم","رعیت پیشه","1","42","221","28","0","0","20/12/2025 10:31:12.52","SP","SP","0","WH_HAS_REF"
|
||||||
|
"88","09172606097","کریم","رعیت پیشه","1","42","248","55","0","0","20/12/2025 10:39:58.833333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"89","09171237952","نورالله","هاشمی ","1","10","117","9","56000000","112000000","19/11/2025 01:24:24",,,"0","WH_HAS_REF"
|
||||||
|
"89","09171237952","نورالله","هاشمی ","1","10","336","130","-44675600","0","10/2/2026 14:35:23.1658124","89","89","0","WH_HAS_REF"
|
||||||
|
"89","09171237952","نورالله","هاشمی ","1","10","339","133","-875000","0","10/2/2026 22:07:11.6883998","89","89","0","WH_HAS_REF"
|
||||||
|
"90","09174337813","علیرضا ","پولادی","1","43","127","55","56000000","112000000","3/12/2025 09:49:45.92","09174337813","09174337813","0","WH_HAS_REF"
|
||||||
|
"90","09174337813","علیرضا ","پولادی","1","43","10367","10176","-21760076","0","31/3/2026 19:42:52.3320407","90","90","0","WH_HAS_REF"
|
||||||
|
"90","09174337813","علیرضا ","پولادی","1","43","60688","60442","-33815504","0","13/7/2026 20:26:12.8880784","90","90","0","WH_HAS_REF"
|
||||||
|
"91","09390081668","رحیم ","رعیت پیشه","1","44","136","40","56000000","112000000","3/12/2025 09:49:45.92","09390081668","09390081668","0","WH_HAS_REF"
|
||||||
|
"93","09122156286","ریحانه سادات ","هاشمی نصر ","3","46","103","24","56000000","0","19/11/2025 01:24:24",,,"1","WH_HAS_REF"
|
||||||
|
"93","09122156286","ریحانه سادات ","هاشمی نصر ","3","46","40579","40339","-47047000","0","15/6/2026 09:29:00.5576873","93","93","0","WH_HAS_REF"
|
||||||
|
"93","09122156286","ریحانه سادات ","هاشمی نصر ","3","46","40580","40340","-7084000","0","15/6/2026 16:07:15.3974267","93","93","0","WH_HAS_REF"
|
||||||
|
"93","09122156286","ریحانه سادات ","هاشمی نصر ","3","46","40628","40389","-1532749","0","21/6/2026 12:13:01.1948474","93","93","0","WH_HAS_REF"
|
||||||
|
"93","09122156286","ریحانه سادات ","هاشمی نصر ","3","46","40629","40390","-336050","0","21/6/2026 12:15:06.3540102","93","93","0","WH_HAS_REF"
|
||||||
|
"96","09125020729","غلامرضا ","عبدی ","3","47","104","25","56000000","0","19/11/2025 01:24:24",,,"1","WH_HAS_REF"
|
||||||
|
"96","09125020729","غلامرضا ","عبدی ","3","47","165","78","-13585000","0","18/12/2025 15:28:38.216666","96","96","0","WH_HAS_REF"
|
||||||
|
"96","09125020729","غلامرضا ","عبدی ","3","47","167","80","-13585000","0","18/12/2025 15:31:34.153333","96","96","0","WH_HAS_REF"
|
||||||
|
"96","09125020729","غلامرضا ","عبدی ","3","47","169","82","-13585000","0","18/12/2025 15:35:01.376666","96","96","0","WH_HAS_REF"
|
||||||
|
"96","09125020729","غلامرضا ","عبدی ","3","47","170","83","-13585000","0","18/12/2025 15:36:41.913333","96","96","0","WH_HAS_REF"
|
||||||
|
"96","09125020729","غلامرضا ","عبدی ","3","47","306","115","-1403000","0","5/1/2026 09:33:31.2176626","96","96","0","WH_HAS_REF"
|
||||||
|
"96","09125020729","غلامرضا ","عبدی ","3","47","40409","40212","-165000","0","11/5/2026 16:21:57.7690801","96","96","0","WH_HAS_REF"
|
||||||
|
"99","09106519822","هستی","خادمی","3","49","105","26","56000000","0","19/11/2025 01:24:24",,,"1","WH_HAS_REF"
|
||||||
|
"103","09384366040","علی ","دلال زاده","3","51","142","46","56000000","0","3/12/2025 09:49:45.92","09384366040","09384366040","1","WH_HAS_REF"
|
||||||
|
"103","09384366040","علی ","دلال زاده","3","51","20368","20177","-56000000","0","3/4/2026 14:14:43.1146196","103","103","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","40483","126","0","0","29/5/2026 18:46:36.0866667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","40486","129","0","0","29/5/2026 18:46:36.1866667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","40511","40288","-22399999","0","1/6/2026 10:34:09.2649307","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","40513","40290","-5270100","0","1/6/2026 10:39:29.0610185","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","40658","158","0","0","27/6/2026 11:52:47.0866667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","50672","165","0","0","5/7/2026 09:38:54.27","SP","SP","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","70688","20166","0","0","18/7/2026 06:15:56.3666667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","70704","70452","2500000","0","19/7/2026 15:06:29.9828496","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","110829","30189","0","0","7/8/2026 22:33:52.54","SP","SP","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","110884","110650","-5307351","0","11/8/2026 11:26:39.2456538","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","110885","110651","100000000","0","11/8/2026 13:22:21.9329329","162","162","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","120891","30206","0","0","15/8/2026 05:48:36.46","SP","SP","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","130884","130651","-1596001","0","17/8/2026 07:35:38.158927","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","130885","110279","1596001","0","17/8/2026 07:37:37.1659725","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","130886","130652","-1596001","0","17/8/2026 07:38:51.0009125","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","130908","130676","-1562000","0","19/8/2026 06:44:00.2389859","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","130912","130682","-4389361","0","20/8/2026 04:58:46.4329118","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","130913","130683","-4398796","0","20/8/2026 05:01:45.1576458","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","130914","110293","4389361","0","20/8/2026 05:02:55.1041528","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","130915","130684","-4468561","0","20/8/2026 05:19:39.1097062","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","140904","140673","-1610400","0","22/8/2026 06:56:21.7082457","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","140939","140698","-959200","0","24/8/2026 13:27:48.539407","106","106","0","WH_HAS_REF"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","54","140945","140706","-9458500","0","24/8/2026 20:30:02.7379001","106","106","0","WH_HAS_REF"
|
||||||
|
"111","09352828086","انسیه","توسلیان","1","59","129","33","56000000","112000000","3/12/2025 09:49:45.92",,,"0","WH_HAS_REF"
|
||||||
|
"111","09352828086","انسیه","توسلیان","1","59","191","112","0","0","19/12/2025 21:05:21.956666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"111","09352828086","انسیه","توسلیان","1","59","229","36","0","0","20/12/2025 10:31:24.903333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"111","09352828086","انسیه","توسلیان","1","59","256","63","0","0","20/12/2025 10:41:04.043333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"111","09352828086","انسیه","توسلیان","1","59","361","166","-28250200","0","27/2/2026 07:07:04.5068205","111","111","0","WH_HAS_REF"
|
||||||
|
"111","09352828086","انسیه","توسلیان","1","59","40401","109","0","0","10/5/2026 20:32:00.55","SP","SP","0","WH_HAS_REF"
|
||||||
|
"111","09352828086","انسیه","توسلیان","1","59","40487","130","0","0","29/5/2026 18:46:36.1866667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"113","09177325630","کاوس","بیگ اینالو","3","61","141","45","56000000","0","3/12/2025 09:49:45.92","09177325630","09177325630","1","WH_HAS_REF"
|
||||||
|
"113","09177325630","کاوس","بیگ اینالو","3","61","200","115","0","0","19/12/2025 22:00:40.276666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"113","09177325630","کاوس","بیگ اینالو","3","61","232","39","0","0","20/12/2025 10:31:48.963333","SP","SP","0","WH_HAS_REF"
|
||||||
|
"113","09177325630","کاوس","بیگ اینالو","3","61","259","66","0","0","20/12/2025 10:42:03.116666","SP","SP","0","WH_HAS_REF"
|
||||||
|
"115","09056365323","حامد","دهقانی","3","63","139","43","56000000","0","3/12/2025 09:49:45.92","09056365323","09056365323","1","WH_HAS_REF"
|
||||||
|
"115","09056365323","حامد","دهقانی","3","63","168","81","-13585000","0","18/12/2025 15:32:59.06","115","115","0","WH_HAS_REF"
|
||||||
|
"115","09056365323","حامد","دهقانی","3","63","40410","40213","-42295000","0","11/5/2026 16:54:10.9533071","115","115","0","WH_HAS_REF"
|
||||||
|
"115","09056365323","حامد","دهقانی","3","63","40522","40299","0","45000","3/6/2026 09:59:49.8074331",,,"0","WH_HAS_REF"
|
||||||
|
"115","09056365323","حامد","دهقانی","3","63","120892","30207","0","0","15/8/2026 05:48:36.46","SP","SP","0","WH_HAS_REF"
|
||||||
|
"116","09014638673","سالار","رضائی","3","64","138","42","56000000","0","3/12/2025 09:49:45.92","09014638673","09014638673","1","WH_HAS_REF"
|
||||||
|
"116","09014638673","سالار","رضائی","3","64","296","111","-13585000","0","1/1/2026 22:05:44.3947747","116","116","0","WH_HAS_REF"
|
||||||
|
"116","09014638673","سالار","رضائی","3","64","351","145","-21418500","0","20/2/2026 09:00:54.7167888","116","116","0","WH_HAS_REF"
|
||||||
|
"116","09014638673","سالار","رضائی","3","64","356","155","-4600200","0","23/2/2026 09:56:49.6353806","116","116","0","WH_HAS_REF"
|
||||||
|
"116","09014638673","سالار","رضائی","3","64","40376","40189","-8250000","0","28/4/2026 10:36:23.4852403","116","116","0","WH_HAS_REF"
|
||||||
|
"119","09126092994","علیرضا ","کریمی پیروز ","3","67","140","44","56000000","0","3/12/2025 09:49:45.92","09126092994","09126092994","1","WH_HAS_REF"
|
||||||
|
"119","09126092994","علیرضا ","کریمی پیروز ","3","67","100751","100515","-13068000","0","30/7/2026 11:01:44.0341615","119","119","0","WH_HAS_REF"
|
||||||
|
"119","09126092994","علیرضا ","کریمی پیروز ","3","67","100752","100516","-21725401","0","30/7/2026 11:15:29.2485564","119","119","0","WH_HAS_REF"
|
||||||
|
"119","09126092994","علیرضا ","کریمی پیروز ","3","67","100754","100518","-12947000","0","30/7/2026 11:30:36.0769629","119","119","0","WH_HAS_REF"
|
||||||
|
"119","09126092994","علیرضا ","کریمی پیروز ","3","67","100757","100521","-7805504","0","30/7/2026 11:42:40.6883097","119","119","0","WH_HAS_REF"
|
||||||
|
"119","09126092994","علیرضا ","کریمی پیروز ","3","67","100758","100522","143705","0","30/7/2026 11:47:05.028365","119","119","0","WH_HAS_REF"
|
||||||
|
"119","09126092994","علیرضا ","کریمی پیروز ","3","67","100759","100523","-597800","0","30/7/2026 11:47:13.2421718","119","119","0","WH_HAS_REF"
|
||||||
|
"121","09332726075","محمد","جعفری","1","69","137","41","56000000","112000000","3/12/2025 09:49:45.92","09332726075","09332726075","0","WH_HAS_REF"
|
||||||
|
"121","09332726075","محمد","جعفری","1","69","360","165","-55899863","0","25/2/2026 15:47:25.4690474","121","121","0","WH_HAS_REF"
|
||||||
|
"125","09126302293","محمود","دادگرکریمی","1","73","134","38","56000000","112000000","3/12/2025 09:49:45.92","09126302293","09126302293","0","WH_HAS_REF"
|
||||||
|
"125","09126302293","محمود","دادگرکریمی","1","73","348","142","-7739000","0","18/2/2026 17:02:54.3278655","125","125","0","WH_HAS_REF"
|
||||||
|
"125","09126302293","محمود","دادگرکریمی","1","73","40519","40296","-47080000","0","2/6/2026 12:04:37.0376162","125","125","0","WH_HAS_REF"
|
||||||
|
"125","09126302293","محمود","دادگرکریمی","1","73","40520","40297","-1155000","0","2/6/2026 12:11:18.8475273","125","125","0","WH_HAS_REF"
|
||||||
|
"130","09368743289","مهرداد","شجاعی","3","78","40659","159","0","0","27/6/2026 11:52:47.0866667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"130","09368743289","مهرداد","شجاعی","3","78","70689","20167","0","0","18/7/2026 06:15:56.3666667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"162","09199877503","مسعود","آفرین مقدم","3","110","201","0","1","112000000","20/12/2025 02:24:42.771904","92","162","1","WH_NO_REF"
|
||||||
|
"162","09199877503","مسعود","آفرین مقدم","3","110","362","167","-96800","0","28/2/2026 02:16:53.1467394","162","162","0","WH_HAS_REF"
|
||||||
|
"162","09199877503","مسعود","آفرین مقدم","3","110","40651","40415","10000","0","26/6/2026 18:12:30.3695252","162","162","0","WH_HAS_REF"
|
||||||
|
"169","09122583411","مهرداد","شجاعی","3","117","40458","119","0","0","23/5/2026 21:18:37.59","SP","SP","0","WH_HAS_REF"
|
||||||
|
"169","09122583411","مهرداد","شجاعی","3","117","40561","142","0","0","13/6/2026 13:03:08.3066667","SP","SP","0","WH_HAS_REF"
|
||||||
|
"169","09122583411","مهرداد","شجاعی","3","117","140907","40205","0","0","22/8/2026 09:15:24.6","SP","SP","0","WH_HAS_REF"
|
||||||
|
"170","09120991323","لیلا","خدارحمی دره میانه","3","118","292","107","56000000","112000000","30/12/2025 01:24:24","SP","SP","1","WH_HAS_REF"
|
||||||
|
"171","09375724408","مژده","بنی هاشمی","3","119","293","108","56000000","112000000","31/12/2025 01:24:24","SP","SP","1","WH_HAS_REF"
|
||||||
|
"171","09375724408","مژده","بنی هاشمی","3","119","300","87","0","0","3/1/2026 09:05:11.38","SP","SP","0","WH_HAS_REF"
|
||||||
|
"171","09375724408","مژده","بنی هاشمی","3","119","308","117","-13585000","0","8/1/2026 08:46:14.5070996","171","171","0","WH_HAS_REF"
|
||||||
|
"171","09375724408","مژده","بنی هاشمی","3","119","40403","40206","-28121199","0","11/5/2026 11:24:40.3004634","171","171","0","WH_HAS_REF"
|
||||||
|
"171","09375724408","مژده","بنی هاشمی","3","119","40404","40207","-660000","0","11/5/2026 12:08:37.0557993","171","171","0","WH_HAS_REF"
|
||||||
|
"172","09121355832","مهرافشان","زاهدنیا","3","120","294","109","56000000","112000000","31/12/2025 01:24:24","SP","SP","1","WH_HAS_REF"
|
||||||
|
"172","09121355832","مهرافشان","زاهدنیا","3","120","40434","40235","-56000000","0","17/5/2026 06:24:52.3451604","172","172","0","WH_HAS_REF"
|
||||||
|
"175","09122332219","ناهید ","حسن زاده مژدهی","3","123","295","110","56000000","112000000","1/1/2026 17:07:53.2301978","SP","SP","1","WH_HAS_REF"
|
||||||
|
"175","09122332219","ناهید ","حسن زاده مژدهی","3","123","40405","40208","-56000000","0","11/5/2026 13:21:51.1259865","175","175","0","WH_HAS_REF"
|
||||||
|
"2","09387342688","رباب","مرجانی","3","3","1","1","40654","154","1","154","0",,"0",,"1","0",,"1",,"WH_REF_BROKEN_NEED_CREATE_TX","Create missing Transaction; link WH+UPP"
|
||||||
|
"10","09109394124","مهدی","صیفی","3","1","1","0",,,"0",,"0",,"0",,"1","0",,"1",,"WH_REF_BROKEN_NEED_CREATE_TX","Create missing Transaction; link WH+UPP"
|
||||||
|
"11","09303303391","مهدی","مرجانی","3","1","1","0",,,"0",,"0",,"0",,"1","0",,"1",,"WH_REF_BROKEN_NEED_CREATE_TX","Create missing Transaction; link WH+UPP"
|
||||||
|
"12","09333773152","مهدی","مرجانی","3","1","1","0",,,"0",,"0",,"0",,"1","0",,"1",,"WH_REF_BROKEN_NEED_CREATE_TX","Create missing Transaction; link WH+UPP"
|
||||||
|
"13","09382734852","مهدی","مرجانی","3","1","1","0",,,"0",,"0",,"0",,"1","0",,"1",,"WH_REF_BROKEN_NEED_CREATE_TX","Create missing Transaction; link WH+UPP"
|
||||||
|
"41","09124287589","مهدی","مرجانی","3","1","1","0",,,"0",,"0",,"0",,"1","0",,"1",,"WH_REF_BROKEN_NEED_CREATE_TX","Create missing Transaction; link WH+UPP"
|
||||||
|
"69","09217672935","مهدی ","گیوی ","3","4","3","1","40430","111","1","111","1","58","3","58","1","0",,"1",,"WH_REF_BROKEN_TX_EXISTS_FIX_LINK","Link WH.RefrenceId to Tx from CreatedBy hint; ensure UPP"
|
||||||
|
"71","09015793338","حمیدرضا","مرجانی","3","6","3","1","110827","40329","2","40329","1","56","3","56","1","0",,"1",,"WH_REF_BROKEN_TX_EXISTS_FIX_LINK","Link WH.RefrenceId to Tx from CreatedBy hint; ensure UPP"
|
||||||
|
"72","09124678697","مژگان","گیوی","3","1","1","1","124","0","0",,"0",,"1","57","1","0",,"1",,"WH_REF_BROKEN_TX_EXISTS_FIX_LINK","Link WH.RefrenceId to Tx from CreatedBy hint; ensure UPP"
|
||||||
|
"74","09101692049","سجاد ","اداوی","3","8","3","1","40532","134","5","134","1","59","3","59","1","0",,"1",,"WH_REF_BROKEN_TX_EXISTS_FIX_LINK","Link WH.RefrenceId to Tx from CreatedBy hint; ensure UPP"
|
||||||
|
"105","09382302660","مهرداد","شجاعی","3","1","1","0",,,"0",,"0",,"0",,"1","0",,"1",,"WH_REF_BROKEN_NEED_CREATE_TX","Create missing Transaction; link WH+UPP"
|
||||||
|
"142","09399172812","صغری","شبانکاره ","3","1","1","0",,,"0",,"1","68","0",,"1","0",,"1",,"WH_REF_BROKEN_TX_BY_MOBILE_FIX_LINK","Link WH.RefrenceId to Tx found by mobile; ensure UPP"
|
||||||
|
"162","09199877503","مسعود","آفرین مقدم","3","3","1","1","40651","40415","2","40415","14","40327","0",,"1","0",,"1",,"WH_REF_BROKEN_TX_BY_MOBILE_FIX_LINK","Link WH.RefrenceId to Tx found by mobile; ensure UPP"
|
||||||
|
"176","09214456946","مهریدخت ","میکانیکی","3","1","1","0",,,"0",,"0",,"0",,"1","0",,"1",,"WH_REF_BROKEN_NEED_CREATE_TX","Create missing Transaction; link WH+UPP"
|
||||||
|
"7","09128676640","مهدی","صیفی","3","26","0","0","120888","30203","23","20177","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"8","09126612405","مهدی","صیفی","3","3","0","0","90717","20178","2","156","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"9","09387095611","مهدی","مرجانی","3","11","0","0","40485","128","10","128","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"40","09124287588","مهدی","مرجانی","3","12","0","0","140905","40203","9","40203","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"43","09039033341","کریم","خادمی ","3","27","0","1","140906","110606","21","110606","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"45","09121150062","علی","دلال زاده","3","12","0","1","100755","100519","12","100519","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"47","09123122544","صدیقه","منعمیان","3","18","0","1","40500","40275","17","40275","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"52","09178327020","حمیدرضا ","اسمعیلی ","3","1","0","1","122","31","1","31","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"58","09362102420","وحید","حق گو","1","1","0","0","115","7","1","7","0",,"0",,"1","0",,"1",,"OK_WH+TX","Fix PurchaseMethod 1→3 only"
|
||||||
|
"75","09353851578","پویا","عبدی ","3","20","0","1","120890","40295","17","40295","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"87","09020569440","امیررضا","محمدی","1","4","0","0","247","103","4","103","0",,"0",,"1","0",,"1",,"OK_WH+TX","Fix PurchaseMethod 1→3 only"
|
||||||
|
"88","09172606097","کریم","رعیت پیشه","1","4","0","0","248","104","4","104","1","53","0",,"1","0",,"1",,"OK_WH+TX","Fix PurchaseMethod 1→3 only"
|
||||||
|
"89","09171237952","نورالله","هاشمی ","1","3","0","0","339","133","3","133","0",,"0",,"1","0",,"1",,"OK_WH+TX","Fix PurchaseMethod 1→3 only"
|
||||||
|
"90","09174337813","علیرضا ","پولادی","1","3","0","0","60688","60442","3","60442","1","55","0",,"1","0",,"1",,"OK_WH+TX","Fix PurchaseMethod 1→3 only"
|
||||||
|
"91","09390081668","رحیم ","رعیت پیشه","1","1","0","0","136","40","1","40","1","40","0",,"1","0",,"1",,"OK_WH+TX","Fix PurchaseMethod 1→3 only"
|
||||||
|
"93","09122156286","ریحانه سادات ","هاشمی نصر ","3","5","0","1","40629","40390","5","40390","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"96","09125020729","غلامرضا ","عبدی ","3","7","0","1","40409","40212","7","40212","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"99","09106519822","هستی","خادمی","3","1","0","1","105","26","1","26","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"103","09384366040","علی ","دلال زاده","3","2","0","1","20368","20177","2","20177","1","46","0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"106","09122302660","مهرداد","شجاعی","3","23","0","0","140945","140706","18","140706","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"111","09352828086","انسیه","توسلیان","1","7","0","0","40487","166","7","166","0",,"0",,"1","0",,"1",,"OK_WH+TX","Fix PurchaseMethod 1→3 only"
|
||||||
|
"113","09177325630","کاوس","بیگ اینالو","3","4","0","1","259","115","4","115","1","45","0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"115","09056365323","حامد","دهقانی","3","5","0","1","120892","40299","4","40299","1","43","0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"116","09014638673","سالار","رضائی","3","5","0","1","40376","40189","5","40189","1","42","0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"119","09126092994","علیرضا ","کریمی پیروز ","3","7","0","1","100759","100523","7","100523","1","44","0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"121","09332726075","محمد","جعفری","1","2","0","0","360","165","2","165","1","41","0",,"1","0",,"1",,"OK_WH+TX","Fix PurchaseMethod 1→3 only"
|
||||||
|
"125","09126302293","محمود","دادگرکریمی","1","4","0","0","40520","40297","4","40297","1","38","0",,"1","0",,"1",,"OK_WH+TX","Fix PurchaseMethod 1→3 only"
|
||||||
|
"130","09368743289","مهرداد","شجاعی","3","2","0","0","70689","20167","1","159","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"169","09122583411","مهرداد","شجاعی","3","3","0","0","140907","40205","3","40205","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"170","09120991323","لیلا","خدارحمی دره میانه","3","1","0","1","292","107","1","107","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"171","09375724408","مژده","بنی هاشمی","3","5","0","1","40404","40207","4","40207","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"172","09121355832","مهرافشان","زاهدنیا","3","2","0","1","40434","40235","2","40235","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"175","09122332219","ناهید ","حسن زاده مژدهی","3","2","0","1","40405","40208","2","40208","0",,"0",,"1","0",,"1",,"OK_WH+TX","Likely OK — verify PurchaseMethod"
|
||||||
|
"HAS_WH+TX_NEED_UPP","33"
|
||||||
|
"NO_WALLET_HISTORY","8"
|
||||||
|
"WH_REF_BROKEN_TX_EXISTS_FIX_LINK","4"
|
||||||
|
"WH_REF_BROKEN_NEED_CREATE_TX","1"
|
||||||
|
"WH_REF_BROKEN_TX_BY_MOBILE_FIX_LINK","1"
|
||||||
|
"23","EXISTS","56000000","اعتبار دایا","manualy24",,,,,"102","38","23",,
|
||||||
|
"23","EXISTS","56000000","اعتبار دایا","manualy24",,,,,"216","24","23",,
|
||||||
|
"24","EXISTS","56000000","اعتبار دایا","manualy25",,,,,"103","46","24",,
|
||||||
|
"24","EXISTS","56000000","اعتبار دایا","manualy25",,,,,"217","6","24",,
|
||||||
|
"25","EXISTS","56000000","اعتبار دایا","manualy26",,,,,"104","47","25",,
|
||||||
|
"26","EXISTS","56000000","اعتبار دایا","manualy27",,,,,"105","49","26",,
|
||||||
|
"28","EXISTS","56000000","اعتبار دایا","manualy29",,,,,"106","24","28",,
|
||||||
|
"28","EXISTS","56000000","اعتبار دایا","manualy29",,,,,"221","42","28",,
|
||||||
|
"29","EXISTS","56000000","اعتبار دایا","manualy30",,,,,"107","22","29",,
|
||||||
|
"29","EXISTS","56000000","اعتبار دایا","manualy30",,,,,"222","62","29",,
|
||||||
|
"30","EXISTS","56000000","اعتبار دایا","manualy31",,,,,"108","23","30",,
|
||||||
|
"30","EXISTS","56000000","اعتبار دایا","manualy31",,,,,"223","13","30",,
|
||||||
|
"31","EXISTS","56000000","اعتبار دایا","manualy32",,,,,"122","26","31",,
|
||||||
|
"31","EXISTS","56000000","اعتبار دایا","manualy32",,,,,"224","15","31",,
|
||||||
|
"42","EXISTS","56000000","اعتبار دایا","manualy43","09014638673","09014638673","116","09014638673","138","64","42",,
|
||||||
|
"42","EXISTS","56000000","اعتبار دایا","manualy43","09014638673","09014638673","116","09014638673","235","24","42",,
|
||||||
|
"43","EXISTS","56000000","اعتبار دایا","manualy44","09056365323","09056365323","115","09056365323","139","63","43",,
|
||||||
|
"44","EXISTS","56000000","اعتبار دایا","manualy45","09126092994","09126092994","119","09126092994","140","67","44",,
|
||||||
|
"44","EXISTS","56000000","اعتبار دایا","manualy45","09126092994","09126092994","119","09126092994","237","38","44",,
|
||||||
|
"45","EXISTS","56000000","اعتبار دایا","manualy46","09177325630","09177325630","113","09177325630","141","61","45",,
|
||||||
|
"45","EXISTS","56000000","اعتبار دایا","manualy46","09177325630","09177325630","113","09177325630","238","6","45",,
|
||||||
|
"46","EXISTS","56000000","اعتبار دایا","manualy47","09384366040","09384366040","103","09384366040","142","51","46",,
|
||||||
|
"46","EXISTS","56000000","اعتبار دایا","manualy47","09384366040","09384366040","103","09384366040","239","13","46",,
|
||||||
|
"56","EXISTS","56000000","اعتبار دایا","manualy57","09015793338","09015793338","71","09015793338","249","62","56",,
|
||||||
|
"57","EXISTS","56000000","اعتبار دایا","manualy58","09961354257","09961354257",,,"250","13","57",,
|
||||||
|
"58","EXISTS","56000000","اعتبار دایا","manualy59","09217672935","09217672935","69","09217672935","251","15","58",,
|
||||||
|
"59","EXISTS","56000000","اعتبار دایا","manualy60","09101692049","09101692049","74","09101692049","252","22","59",,
|
||||||
|
"68","EXISTS","56000000","اعتبار دایا","manualy69","09399172812","09399172812","142","09399172812",,,,,
|
||||||
|
"107","EXISTS","56000000","اعتبار پرداخت مستقیم","manualy292",,,,,"186","15","107",,
|
||||||
|
"107","EXISTS","56000000","اعتبار پرداخت مستقیم","manualy292",,,,,"292","118","107",,
|
||||||
|
"107","EXISTS","56000000","اعتبار پرداخت مستقیم","manualy292",,,,,"40399","34","107",,
|
||||||
|
"108","EXISTS","56000000","اعتبار پرداخت مستقیم","manualy293",,,,,"187","22","108",,
|
||||||
|
"108","EXISTS","56000000","اعتبار پرداخت مستقیم","manualy293",,,,,"293","119","108",,
|
||||||
|
"108","EXISTS","56000000","اعتبار پرداخت مستقیم","manualy293",,,,,"40400","38","108",,
|
||||||
|
"109","EXISTS","56000000","اعتبار پرداخت مستقیم","manualy294",,,,,"188","23","109",,
|
||||||
|
"109","EXISTS","56000000","اعتبار پرداخت مستقیم","manualy294",,,,,"294","120","109",,
|
||||||
|
"109","EXISTS","56000000","اعتبار پرداخت مستقیم","manualy294",,,,,"40401","59","109",,
|
||||||
|
"110","EXISTS","56000000","اعتبار پرداخت مستقیم","manualy295",,,,,"189","24","110",,
|
||||||
|
"110","EXISTS","56000000","اعتبار پرداخت مستقیم","manualy295",,,,,"295","123","110",,
|
||||||
|
"110","EXISTS","56000000","اعتبار پرداخت مستقیم","manualy295",,,,,"40402","134","110",,
|
||||||
|
"102","38","23","56000000",,,"75","09353851578",,,"23","اعتبار دایا",,,"WH_LINKED_TO_TX"
|
||||||
|
"103","46","24","56000000",,,"93","09122156286",,,"24","اعتبار دایا",,,"WH_LINKED_TO_TX"
|
||||||
|
"104","47","25","56000000",,,"96","09125020729",,,"25","اعتبار دایا",,,"WH_LINKED_TO_TX"
|
||||||
|
"105","49","26","56000000",,,"99","09106519822",,,"26","اعتبار دایا",,,"WH_LINKED_TO_TX"
|
||||||
|
"106","24","28","56000000",,,"47","09123122544",,,"28","اعتبار دایا",,,"WH_LINKED_TO_TX"
|
||||||
|
"107","22","29","56000000",,,"43","09039033341",,,"29","اعتبار دایا",,,"WH_LINKED_TO_TX"
|
||||||
|
"108","23","30","56000000",,,"45","09121150062",,,"30","اعتبار دایا",,,"WH_LINKED_TO_TX"
|
||||||
|
"118","34","0","1","56","09015793338","71","09015793338","71","09015793338",,,"56","اعتبار دایا","WH_REF_BROKEN_USE_CREATEDBY_HINT"
|
||||||
|
"119","32","0","1","58","09217672935","69","09217672935","69","09217672935",,,"58","اعتبار دایا","WH_REF_BROKEN_USE_CREATEDBY_HINT"
|
||||||
|
"120","37","0","1","59","09101692049","74","09101692049","74","09101692049",,,"59","اعتبار دایا","WH_REF_BROKEN_USE_CREATEDBY_HINT"
|
||||||
|
"121","12","0","1","0","09387342688","2","09387342688","2","09387342688",,,,,"WH_FOUND_USER_BY_MOBILE_NO_TX_LINK"
|
||||||
|
"122","26","31","56000000",,,"52","09178327020",,,"31","اعتبار دایا",,,"WH_LINKED_TO_TX"
|
||||||
|
"124","35","0","1","57","09961354257","72","09124678697",,,,,"57","اعتبار دایا","WH_REF_BROKEN_USE_CREATEDBY_HINT"
|
||||||
|
"138","64","42","56000000","09014638673","09014638673","116","09014638673","116","09014638673","42","اعتبار دایا",,,"WH_LINKED_TO_TX"
|
||||||
|
"139","63","43","56000000","09056365323","09056365323","115","09056365323","115","09056365323","43","اعتبار دایا",,,"WH_LINKED_TO_TX"
|
||||||
|
"140","67","44","56000000","09126092994","09126092994","119","09126092994","119","09126092994","44","اعتبار دایا",,,"WH_LINKED_TO_TX"
|
||||||
|
"141","61","45","56000000","09177325630","09177325630","113","09177325630","113","09177325630","45","اعتبار دایا",,,"WH_LINKED_TO_TX"
|
||||||
|
"142","51","46","56000000","09384366040","09384366040","103","09384366040","103","09384366040","46","اعتبار دایا",,,"WH_LINKED_TO_TX"
|
||||||
|
"201","110","0","1","92","162","162","09199877503",,,,,,,"WH_ORPHAN_REVIEW"
|
||||||
|
"292","118","107","56000000","SP","SP","170","09120991323",,,"107","اعتبار پرداخت مستقیم",,,"WH_LINKED_TO_TX"
|
||||||
|
"293","119","108","56000000","SP","SP","171","09375724408",,,"108","اعتبار پرداخت مستقیم",,,"WH_LINKED_TO_TX"
|
||||||
|
"294","120","109","56000000","SP","SP","172","09121355832",,,"109","اعتبار پرداخت مستقیم",,,"WH_LINKED_TO_TX"
|
||||||
|
"295","123","110","56000000","SP","SP","175","09122332219",,,"110","اعتبار پرداخت مستقیم",,,"WH_LINKED_TO_TX"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user