Compare commits
47 Commits
9f1bc2bcdb
...
kub-stage
| Author | SHA1 | Date | |
|---|---|---|---|
| ba3a928bd0 | |||
| e7eb50ec35 | |||
| fe7179f6e9 | |||
| f79affb44d | |||
| 6f573c0a29 | |||
| 9b82b45e46 | |||
| cbc1225562 | |||
| 6ad1341754 | |||
| e970e516ce | |||
| 9d17f174af | |||
| 29426022aa | |||
| 2b958bd214 | |||
| f31fb5ee39 | |||
| 0b7212ac1c | |||
| 78d1fee89a | |||
| 612d6fd141 | |||
| 87d61abc5b | |||
| ea1d2d4400 | |||
| 2d23dbc798 | |||
| 8ec0910cdf | |||
| e882f09955 | |||
| 6a04a6a270 | |||
| 427d2cce1d | |||
| fb9e3d0109 | |||
| 6c90e4caf0 | |||
| 40da1bd8df | |||
| 3810146651 | |||
| ce98efdd68 | |||
| d232399f43 | |||
| 246d893b62 | |||
| 728f28f525 | |||
| 89af07c122 | |||
| 58aeddf004 | |||
| 6c3fbe43b1 | |||
| 7570c39e65 | |||
| 46ac1d817e | |||
| db20d95b43 | |||
| f8794bbb63 | |||
| 6395f63ab5 | |||
| 758640cce3 | |||
| 37820ea257 | |||
| 5f8e30905d | |||
| 02b2966e96 | |||
| 29fa7ff435 | |||
| dba0b63a70 | |||
| 541b0c2ce9 | |||
| 121291eeed |
@@ -6,15 +6,15 @@ on:
|
||||
- kub-stage
|
||||
|
||||
env:
|
||||
REGISTRY: 194.5.195.53:30080
|
||||
REGISTRY: 185.252.31.44:30080
|
||||
IMAGE_NAME: admin/cms
|
||||
K8S_SERVER: 194.5.195.53
|
||||
K8S_SERVER: 185.252.31.44
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 194.5.195.53:32082/docker-sshpass:latest
|
||||
image: 185.252.31.44:32082/docker-sshpass:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- name: Start Docker daemon
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
mkdir -p /etc/docker
|
||||
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||
{
|
||||
"insecure-registries": ["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
|
||||
echo "🚀 Starting Docker daemon..."
|
||||
@@ -52,14 +52,14 @@ jobs:
|
||||
|
||||
- name: Login to Docker registries
|
||||
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
|
||||
|
||||
- name: Publish Protobuf packages
|
||||
run: |
|
||||
echo "📦 Publishing Protobuf packages..."
|
||||
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
|
||||
echo "📦 $proj"
|
||||
dotnet restore "$proj" --configfile src/NuGet.config
|
||||
@@ -93,10 +93,11 @@ jobs:
|
||||
|
||||
# Apply config (Secret) first, then deployment
|
||||
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
||||
kubectl apply -f /tmp/cms-config.yaml &&
|
||||
kubectl apply -f /tmp/cms-deployment.yaml &&
|
||||
kubectl rollout restart deployment/cms &&
|
||||
kubectl rollout status deployment/cms --timeout=180s &&
|
||||
kubectl apply -n kbs-stage -f /tmp/cms-config.yaml &&
|
||||
kubectl apply -n kbs-stage -f /tmp/cms-deployment.yaml &&
|
||||
crictl rmi ${REGISTRY}/${IMAGE_NAME}:latest 2>/dev/null || true &&
|
||||
kubectl -n kbs-stage rollout restart deployment/cms &&
|
||||
kubectl -n kbs-stage rollout status deployment/cms --timeout=180s &&
|
||||
rm -f /tmp/cms-config.yaml /tmp/cms-deployment.yaml
|
||||
"
|
||||
echo "✅ Deployed!"
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- production
|
||||
|
||||
env:
|
||||
REGISTRY: 194.5.195.53:30080
|
||||
REGISTRY: 185.252.31.44:30080
|
||||
IMAGE_NAME: admin/cms
|
||||
K8S_SERVER: 45.149.79.127
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 194.5.195.53:32082/docker-sshpass:latest
|
||||
image: 185.252.31.44:32082/docker-sshpass:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- name: Start Docker daemon
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
mkdir -p /etc/docker
|
||||
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||
{
|
||||
"insecure-registries": ["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
|
||||
echo "🚀 Starting Docker daemon..."
|
||||
@@ -50,14 +50,14 @@ jobs:
|
||||
|
||||
- name: Login to Docker registries
|
||||
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
|
||||
|
||||
- name: Publish Protobuf packages
|
||||
run: |
|
||||
echo "📦 Publishing Protobuf packages..."
|
||||
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
|
||||
echo "📦 $proj"
|
||||
dotnet restore "$proj" --configfile src/NuGet.config
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
dotnet pack "$proj" -c Release --no-build -o "$(dirname $proj)/nupkg"
|
||||
for nupkg in $(dirname $proj)/nupkg/*.nupkg; do
|
||||
[ -f "$nupkg" ] && dotnet nuget push "$nupkg" \
|
||||
--source "http://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" \
|
||||
--skip-duplicate --allow-insecure-connections || true
|
||||
done
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
# Copy NuGet config first
|
||||
@@ -7,11 +7,11 @@ COPY src/NuGet.config ./
|
||||
# Copy solution and project files
|
||||
COPY src/ ./
|
||||
|
||||
# Restore with Nexus config and publish
|
||||
# Restore with Nexus config and publish
|
||||
RUN dotnet restore "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj" --configfile NuGet.config
|
||||
RUN dotnet publish "CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj" -c Release -o /app/publish --no-restore
|
||||
|
||||
FROM 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
|
||||
COPY --from=build /app/publish .
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# CMS Microservice
|
||||
|
||||
Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
|
||||
Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
|
||||
|
||||
@@ -24,6 +24,24 @@ stringData:
|
||||
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
||||
"UseSandbox": false
|
||||
},
|
||||
"TwentyCard": {
|
||||
"BaseUrl": "https://gateway.20card.ir/ipg",
|
||||
"TerminalId": "165674",
|
||||
"TerminalUsername": "ea5y1biehi",
|
||||
"TerminalPassword": "SaLamAT@1404!",
|
||||
"CreditPortalUrl": "https://userpanel.20card.ir/",
|
||||
"DefaultEmail": "info@kbs2.ir",
|
||||
"Packages": {
|
||||
"4": {
|
||||
"GatewayAmountToman": 11200000,
|
||||
"WalletCreditToman": 12656000
|
||||
},
|
||||
"1": {
|
||||
"GatewayAmountToman": 11200000,
|
||||
"WalletCreditToman": 12656000
|
||||
}
|
||||
}
|
||||
},
|
||||
"CmsBaseUrl": "https://cms.kbs2.ir",
|
||||
"FrontOfficeBaseUrl": "https://kbs2.ir",
|
||||
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
||||
|
||||
@@ -35,7 +35,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: cms
|
||||
image: 194.5.195.53:30080/admin/cms:prod
|
||||
image: 185.252.31.44:30080/admin/cms:prod
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
|
||||
@@ -11,7 +11,7 @@ apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: cms-appsettings
|
||||
namespace: default
|
||||
namespace: kbs-stage
|
||||
labels:
|
||||
app: cms
|
||||
environment: staging
|
||||
@@ -24,6 +24,27 @@ stringData:
|
||||
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
||||
"UseSandbox": true
|
||||
},
|
||||
"TwentyCard": {
|
||||
"BaseUrl": "https://gateway.20card.ir/ipg",
|
||||
"TerminalId": "165674",
|
||||
"TerminalUsername": "ea5y1biehi",
|
||||
"TerminalPassword": "SaLamAT@1404!",
|
||||
"CreditPortalUrl": "https://userpanel.20card.ir/",
|
||||
"CallbackBaseUrl": "https://kbs2.ir",
|
||||
"DefaultEmail": "info@kbs2.ir",
|
||||
"Packages": {
|
||||
"4": {
|
||||
"GatewayAmountToman": 11200000,
|
||||
"WalletCreditToman": 12656000
|
||||
},
|
||||
"1": {
|
||||
"GatewayAmountToman": 11200000,
|
||||
"WalletCreditToman": 12656000
|
||||
}
|
||||
}
|
||||
},
|
||||
"CmsBaseUrl": "https://cms.se.kbs1.ir",
|
||||
"FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir",
|
||||
"FMS": {
|
||||
"Address": "https://dl.afrino.co"
|
||||
},
|
||||
@@ -32,7 +53,7 @@ stringData:
|
||||
"JwtAudience": "https://localhost",
|
||||
"JwtExpiryInDays": 5,
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Data Source=194.5.195.53,31433; Initial Catalog=Foursat;User ID=sa;Password=87zH26nbqT;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
|
||||
"DefaultConnection": "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"
|
||||
},
|
||||
"Otp": {
|
||||
@@ -105,7 +126,7 @@ stringData:
|
||||
"Audience": "domain_api"
|
||||
},
|
||||
"Seq": {
|
||||
"ServerUrl": "https://seq.afrino.co",
|
||||
"ServerUrl": "http://iseq.afrino.co",
|
||||
"ApiKey": "oxpvpUzU1pZxMS4s3Fqq"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: cms-uploads-pvc
|
||||
namespace: default
|
||||
namespace: kbs-stage
|
||||
labels:
|
||||
app: cms
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: migrated-local
|
||||
volumeName: pv-cms-uploads-migrated
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
@@ -20,7 +22,7 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: cms
|
||||
namespace: default
|
||||
namespace: kbs-stage
|
||||
labels:
|
||||
app: cms
|
||||
spec:
|
||||
@@ -33,10 +35,12 @@ spec:
|
||||
labels:
|
||||
app: cms
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry-secret
|
||||
containers:
|
||||
- name: cms
|
||||
image: 194.5.195.53:30080/admin/cms:latest
|
||||
imagePullPolicy: Always
|
||||
image: 185.252.31.44:30080/admin/cms:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
@@ -89,7 +93,7 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: cms-svc
|
||||
namespace: default
|
||||
namespace: kbs-stage
|
||||
spec:
|
||||
selector:
|
||||
app: cms
|
||||
@@ -107,7 +111,7 @@ apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: cms-ingress
|
||||
namespace: default
|
||||
namespace: kbs-stage
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
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-send-timeout: "3600"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- cms.se.kbs1.ir
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
-- =============================================================================
|
||||
-- گزارش ساده: ورودیهای هر هفته باشگاه (+ اسامی)
|
||||
-- =============================================================================
|
||||
-- تعریف:
|
||||
-- Users.Created داخل بازه هفته
|
||||
-- و ClubMembership.Created هم داخل همان بازه
|
||||
-- IsActive=1 → فعال | IsActive=0 → غیرفعال
|
||||
-- =============================================================================
|
||||
|
||||
SET NOCOUNT ON;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- ۱) خلاصه هر هفته
|
||||
------------------------------------------------------------
|
||||
SELECT
|
||||
wd.DisplayName AS [هفته],
|
||||
CONVERT(varchar(10), wd.StartDate, 23) AS [از],
|
||||
CONVERT(varchar(10), wd.EndDate, 23) AS [تا],
|
||||
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN u.Id IS NOT NULL AND cm.IsActive = 1 THEN u.Id
|
||||
END) AS [تعداد_فعال],
|
||||
SUM(CASE
|
||||
WHEN u.Id IS NOT NULL AND cm.IsActive = 1
|
||||
THEN ISNULL(NULLIF(cm.InitialContribution, 0), ISNULL(p.ActivationFee, 0))
|
||||
ELSE 0
|
||||
END) AS [مبلغ_فعال],
|
||||
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN u.Id IS NOT NULL AND cm.IsActive = 0 THEN u.Id
|
||||
END) AS [تعداد_غیرفعال],
|
||||
SUM(CASE
|
||||
WHEN u.Id IS NOT NULL AND cm.IsActive = 0
|
||||
THEN ISNULL(NULLIF(cm.InitialContribution, 0), ISNULL(p.ActivationFee, 0))
|
||||
ELSE 0
|
||||
END) AS [مبلغ_غیرفعال],
|
||||
|
||||
ISNULL((
|
||||
SELECT SUM(wp.TotalPoolAmount)
|
||||
FROM CMS.WeeklyCommissionPools wp
|
||||
WHERE wp.WeekDefinitionId = wd.Id
|
||||
AND wp.IsDeleted = 0
|
||||
), 0) AS [استخر_هفته]
|
||||
|
||||
FROM CMS.WeekDefinitions wd
|
||||
LEFT JOIN CMS.Users u
|
||||
ON u.IsDeleted = 0
|
||||
AND u.Created >= wd.StartDate
|
||||
AND u.Created < wd.EndDate
|
||||
LEFT JOIN CMS.ClubMemberships cm
|
||||
ON cm.UserId = u.Id
|
||||
AND cm.IsDeleted = 0
|
||||
AND cm.Created >= wd.StartDate
|
||||
AND cm.Created < wd.EndDate
|
||||
LEFT JOIN CMS.Packages p
|
||||
ON p.Id = cm.LastPackageId
|
||||
AND p.IsDeleted = 0
|
||||
WHERE wd.IsDeleted = 0
|
||||
GROUP BY
|
||||
wd.Id,
|
||||
wd.DisplayName,
|
||||
wd.StartDate,
|
||||
wd.EndDate
|
||||
ORDER BY wd.StartDate DESC;
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
-- ۲) جزئیات اسامی (هر نفر یک ردیف)
|
||||
------------------------------------------------------------
|
||||
SELECT
|
||||
wd.DisplayName AS [هفته],
|
||||
CONVERT(varchar(10), wd.StartDate, 23) AS [از],
|
||||
CONVERT(varchar(10), wd.EndDate, 23) AS [تا],
|
||||
u.Id AS [UserId],
|
||||
LTRIM(RTRIM(ISNULL(u.FirstName, N'') + N' ' + ISNULL(u.LastName, N''))) AS [نام],
|
||||
u.Mobile AS [موبایل],
|
||||
CASE WHEN cm.IsActive = 1 THEN N'فعال' ELSE N'غیرفعال' END AS [وضعیت],
|
||||
ISNULL(NULLIF(cm.InitialContribution, 0), ISNULL(p.ActivationFee, 0)) AS [مبلغ],
|
||||
CONVERT(varchar(19), u.Created, 120) AS [تاریخ_ایجاد_کاربر],
|
||||
CONVERT(varchar(19), cm.Created, 120) AS [تاریخ_ایجاد_عضویت]
|
||||
FROM CMS.WeekDefinitions wd
|
||||
INNER JOIN CMS.Users u
|
||||
ON u.IsDeleted = 0
|
||||
AND u.Created >= wd.StartDate
|
||||
AND u.Created < wd.EndDate
|
||||
INNER JOIN CMS.ClubMemberships cm
|
||||
ON cm.UserId = u.Id
|
||||
AND cm.IsDeleted = 0
|
||||
AND cm.Created >= wd.StartDate
|
||||
AND cm.Created < wd.EndDate
|
||||
LEFT JOIN CMS.Packages p
|
||||
ON p.Id = cm.LastPackageId
|
||||
AND p.IsDeleted = 0
|
||||
WHERE wd.IsDeleted = 0
|
||||
ORDER BY wd.StartDate DESC, cm.IsActive DESC, u.Id;
|
||||
@@ -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
|
||||
+5
-3
@@ -1,3 +1,4 @@
|
||||
using CMSMicroservice.Application.Common;
|
||||
using CMSMicroservice.Application.Common.FileManager;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
@@ -32,12 +33,13 @@ public class CreateBlogPostCommandHandler : IRequestHandler<CreateBlogPostComman
|
||||
if (string.IsNullOrEmpty(currentUserId) || !long.TryParse(currentUserId, out var authorUserId))
|
||||
throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
|
||||
|
||||
var slug = BlogSlugHelper.ResolveForCreate(request.Slug, request.Title.Trim());
|
||||
slug = await BlogSlugHelper.EnsureUniquePostSlugAsync(_context, slug, cancellationToken: cancellationToken);
|
||||
|
||||
var post = new BlogPost
|
||||
{
|
||||
Title = request.Title.Trim(),
|
||||
Slug = string.IsNullOrWhiteSpace(request.Slug)
|
||||
? $"post-{Guid.NewGuid():N}".Substring(0, 20)
|
||||
: request.Slug.Trim().ToLower(),
|
||||
Slug = slug,
|
||||
Summary = request.Summary?.Trim(),
|
||||
HtmlContent = request.HtmlContent,
|
||||
FeaturedImagePath = request.FeaturedImagePath,
|
||||
|
||||
+2
-2
@@ -11,9 +11,9 @@ public class CreateBlogPostCommandValidator : AbstractValidator<CreateBlogPostCo
|
||||
.MaximumLength(200).WithMessage("عنوان نمیتواند بیشتر از 200 کاراکتر باشد");
|
||||
|
||||
RuleFor(x => x.Slug)
|
||||
.NotEmpty().WithMessage("نشانی یکتا (slug) الزامی است")
|
||||
.MaximumLength(200).WithMessage("نشانی نمیتواند بیشتر از 200 کاراکتر باشد")
|
||||
.Matches(@"^[a-z0-9\-]+$").WithMessage("نشانی فقط میتواند شامل حروف کوچک انگلیسی، اعداد و خط تیره باشد");
|
||||
.Matches(@"^[a-z0-9\-]+$").WithMessage("نشانی فقط میتواند شامل حروف کوچک انگلیسی، اعداد و خط تیره باشد")
|
||||
.When(x => !string.IsNullOrWhiteSpace(x.Slug));
|
||||
|
||||
RuleFor(x => x.Summary)
|
||||
.MaximumLength(500).WithMessage("خلاصه نمیتواند بیشتر از 500 کاراکتر باشد")
|
||||
|
||||
+15
-2
@@ -1,3 +1,4 @@
|
||||
using CMSMicroservice.Application.Common;
|
||||
using CMSMicroservice.Application.Common.FileManager;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
@@ -29,9 +30,21 @@ public class UpdateBlogPostCommandHandler : IRequestHandler<UpdateBlogPostComman
|
||||
?? throw new KeyNotFoundException($"مقاله با شناسه {request.Id} یافت نشد");
|
||||
|
||||
post.Title = request.Title.Trim();
|
||||
// حفظ slug قبلی اگر مقدار جدید خالی باشد
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Slug))
|
||||
post.Slug = request.Slug.Trim().ToLower();
|
||||
{
|
||||
post.Slug = await BlogSlugHelper.EnsureUniquePostSlugAsync(
|
||||
_context,
|
||||
BlogSlugHelper.NormalizeOptional(request.Slug)!,
|
||||
post.Id,
|
||||
cancellationToken);
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(post.Slug))
|
||||
{
|
||||
var slug = BlogSlugHelper.ResolveForCreate(null, post.Title);
|
||||
post.Slug = await BlogSlugHelper.EnsureUniquePostSlugAsync(
|
||||
_context, slug, post.Id, cancellationToken);
|
||||
}
|
||||
post.Summary = request.Summary?.Trim();
|
||||
post.HtmlContent = request.HtmlContent;
|
||||
post.FeaturedImagePath = request.FeaturedImagePath;
|
||||
|
||||
+2
-2
@@ -13,9 +13,9 @@ public class UpdateBlogPostCommandValidator : AbstractValidator<UpdateBlogPostCo
|
||||
.MaximumLength(200).WithMessage("عنوان نمیتواند بیشتر از 200 کاراکتر باشد");
|
||||
|
||||
RuleFor(x => x.Slug)
|
||||
.NotEmpty().WithMessage("نشانی یکتا (slug) الزامی است")
|
||||
.MaximumLength(200).WithMessage("نشانی نمیتواند بیشتر از 200 کاراکتر باشد")
|
||||
.Matches(@"^[a-z0-9\-]+$").WithMessage("نشانی فقط میتواند شامل حروف کوچک انگلیسی، اعداد و خط تیره باشد");
|
||||
.Matches(@"^[a-z0-9\-]+$").WithMessage("نشانی فقط میتواند شامل حروف کوچک انگلیسی، اعداد و خط تیره باشد")
|
||||
.When(x => !string.IsNullOrWhiteSpace(x.Slug));
|
||||
|
||||
RuleFor(x => x.HtmlContent)
|
||||
.NotEmpty().WithMessage("محتوای مقاله الزامی است");
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.4.0" />
|
||||
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.10" />
|
||||
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
||||
|
||||
+7
-2
@@ -1,4 +1,5 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Helpers;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Common;
|
||||
@@ -162,10 +163,10 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
request.UserId
|
||||
);
|
||||
|
||||
// در فعالسازی اجباری، اگر PackagePurchaseMethod تنظیم نشده، مقدار DirectPurchase بگذار
|
||||
// در فعالسازی اجباری ادمین، اگر روش خرید تنظیم نشده، دستی در نظر بگیر
|
||||
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
|
||||
{
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.Manual;
|
||||
_context.Users.Update(user);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
@@ -272,6 +273,7 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
foreach (var prevCycle in previousCycles)
|
||||
{
|
||||
prevCycle.IsCurrentCycle = false;
|
||||
ClubMembershipCycleHistoryWriter.WriteCycleClosed(_context, prevCycle, reason: "شروع دوره جدید");
|
||||
}
|
||||
|
||||
var maxCycleNumber = await _context.ClubMembershipCycles
|
||||
@@ -293,6 +295,9 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
_context.ClubMembershipCycles.Add(newCycle);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
ClubMembershipCycleHistoryWriter.WriteCycleStarted(_context, newCycle, reason: "فعالسازی عضویت باشگاه");
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created ClubMembershipCycle #{CycleNumber} for UserId {UserId}, MembershipId {MembershipId}",
|
||||
newCycle.CycleNumber,
|
||||
|
||||
+4
-1
@@ -13,6 +13,7 @@ public class GetAllClubMembershipsQueryHandler : IRequestHandler<GetAllClubMembe
|
||||
{
|
||||
var query = _context.ClubMemberships
|
||||
.Include(x => x.User)
|
||||
.Include(x => x.LastPackage)
|
||||
.ApplyOrder(sortBy: request.SortBy)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
@@ -35,8 +36,10 @@ public class GetAllClubMembershipsQueryHandler : IRequestHandler<GetAllClubMembe
|
||||
Id = x.Id,
|
||||
UserId = x.UserId,
|
||||
UserName = x.User != null ? $"{x.User.FirstName} {x.User.LastName}" : string.Empty,
|
||||
PackageId = x.LastPackageId ?? 0,
|
||||
PackageName = x.LastPackage != null ? x.LastPackage.Title : string.Empty,
|
||||
IsActive = x.IsActive,
|
||||
ActivatedAt = x.ActivatedAt,
|
||||
ActivatedAt = x.LastActivationDate ?? x.ActivatedAt,
|
||||
InitialContribution = x.InitialContribution,
|
||||
TotalEarned = x.TotalEarned,
|
||||
Created = x.Created,
|
||||
|
||||
+2
@@ -11,6 +11,8 @@ public class GetAllClubMembershipsResponseModel
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public long PackageId { get; set; }
|
||||
public string PackageName { get; set; } = string.Empty;
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime? ActivatedAt { get; set; }
|
||||
public long InitialContribution { get; set; }
|
||||
|
||||
+10
-20
@@ -1,27 +1,17 @@
|
||||
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembershipHistory;
|
||||
|
||||
/// <summary>
|
||||
/// Query برای دریافت تاریخچه تغییرات عضویت باشگاه
|
||||
/// </summary>
|
||||
public record GetClubMembershipHistoryQuery : IRequest<GetClubMembershipHistoryResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه عضویت (اختیاری)
|
||||
/// </summary>
|
||||
public long? MembershipId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه کاربر (اختیاری)
|
||||
/// </summary>
|
||||
public long? UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// موقعیت صفحهبندی
|
||||
/// </summary>
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// مرتبسازی بر اساس
|
||||
/// </summary>
|
||||
public string? SortBy { get; init; }
|
||||
public GetClubMembershipHistoryFilter? Filter { get; init; }
|
||||
}
|
||||
|
||||
public class GetClubMembershipHistoryFilter
|
||||
{
|
||||
public long? UserId { get; set; }
|
||||
public long? ClubMembershipId { get; set; }
|
||||
public ClubMembershipAction? Action { get; set; }
|
||||
public DateTime? CreatedFrom { get; set; }
|
||||
public DateTime? CreatedTo { get; set; }
|
||||
}
|
||||
|
||||
+21
-8
@@ -12,20 +12,30 @@ public class GetClubMembershipHistoryQueryHandler : IRequestHandler<GetClubMembe
|
||||
public async Task<GetClubMembershipHistoryResponseDto> Handle(GetClubMembershipHistoryQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.ClubMembershipHistories
|
||||
.Include(x => x.ClubMembership)
|
||||
.ThenInclude(m => m!.User)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
if (request.MembershipId.HasValue)
|
||||
if (request.Filter != null)
|
||||
{
|
||||
query = query.Where(x => x.ClubMembershipId == request.MembershipId.Value);
|
||||
if (request.Filter.ClubMembershipId.HasValue)
|
||||
query = query.Where(x => x.ClubMembershipId == request.Filter.ClubMembershipId.Value);
|
||||
|
||||
if (request.Filter.UserId.HasValue)
|
||||
query = query.Where(x => x.UserId == request.Filter.UserId.Value);
|
||||
|
||||
if (request.Filter.Action.HasValue)
|
||||
query = query.Where(x => x.Action == request.Filter.Action.Value);
|
||||
|
||||
if (request.Filter.CreatedFrom.HasValue)
|
||||
query = query.Where(x => x.Created >= request.Filter.CreatedFrom.Value);
|
||||
|
||||
if (request.Filter.CreatedTo.HasValue)
|
||||
query = query.Where(x => x.Created <= request.Filter.CreatedTo.Value);
|
||||
}
|
||||
|
||||
if (request.UserId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
||||
}
|
||||
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "Created"); // پیشفرض: جدیدترین اول
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
||||
|
||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||
|
||||
@@ -36,6 +46,9 @@ public class GetClubMembershipHistoryQueryHandler : IRequestHandler<GetClubMembe
|
||||
Id = x.Id,
|
||||
ClubMembershipId = x.ClubMembershipId,
|
||||
UserId = x.UserId,
|
||||
UserName = x.ClubMembership != null && x.ClubMembership.User != null
|
||||
? $"{x.ClubMembership.User.FirstName} {x.ClubMembership.User.LastName}"
|
||||
: string.Empty,
|
||||
OldIsActive = x.OldIsActive,
|
||||
NewIsActive = x.NewIsActive,
|
||||
OldInitialContribution = x.OldInitialContribution,
|
||||
|
||||
+1
-26
@@ -4,31 +4,6 @@ public class GetClubMembershipHistoryQueryValidator : AbstractValidator<GetClubM
|
||||
{
|
||||
public GetClubMembershipHistoryQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.MembershipId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه عضویت معتبر نیست")
|
||||
.When(x => x.MembershipId.HasValue);
|
||||
|
||||
RuleFor(x => x.UserId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه کاربر معتبر نیست")
|
||||
.When(x => x.UserId.HasValue);
|
||||
|
||||
RuleFor(x => x)
|
||||
.Must(x => x.MembershipId.HasValue || x.UserId.HasValue)
|
||||
.WithMessage("حداقل یکی از MembershipId یا UserId باید مقداردهی شود");
|
||||
// Admin report: filters are all optional
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
{
|
||||
var result = await ValidateAsync(
|
||||
ValidationContext<GetClubMembershipHistoryQuery>.CreateWithOptions(
|
||||
(GetClubMembershipHistoryQuery)model,
|
||||
x => x.IncludeProperties(propertyName)));
|
||||
|
||||
if (result.IsValid)
|
||||
return Array.Empty<string>();
|
||||
|
||||
return result.Errors.Select(e => e.ErrorMessage);
|
||||
};
|
||||
}
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ public class GetClubMembershipHistoryResponseModel
|
||||
public long Id { get; set; }
|
||||
public long ClubMembershipId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public bool OldIsActive { get; set; }
|
||||
public bool NewIsActive { get; set; }
|
||||
public long? OldInitialContribution { get; set; }
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
using CMSMicroservice.Domain.Entities.Club;
|
||||
|
||||
namespace CMSMicroservice.Application.ClubMembershipCycleCQ.Queries.GetAllClubMembershipCycleByFilter;
|
||||
|
||||
public static class ClubMembershipCycleFilterExtensions
|
||||
{
|
||||
public static IQueryable<ClubMembershipCycle> ApplyClubMembershipCycleFilter(
|
||||
this IQueryable<ClubMembershipCycle> query,
|
||||
GetAllClubMembershipCycleByFilterFilter? filter)
|
||||
{
|
||||
if (filter == null) return query;
|
||||
|
||||
if (filter.UserId.HasValue)
|
||||
query = query.Where(x => x.UserId == filter.UserId.Value);
|
||||
|
||||
if (filter.IsCurrentCycle.HasValue)
|
||||
query = query.Where(x => x.IsCurrentCycle == filter.IsCurrentCycle.Value);
|
||||
|
||||
if (filter.PackageId.HasValue)
|
||||
query = query.Where(x => x.PackageId == filter.PackageId.Value);
|
||||
|
||||
if (filter.CycleNumber.HasValue)
|
||||
query = query.Where(x => x.CycleNumber == filter.CycleNumber.Value);
|
||||
|
||||
if (filter.PurchaseMethod.HasValue)
|
||||
query = query.Where(x => x.PurchaseMethod == filter.PurchaseMethod.Value);
|
||||
|
||||
if (filter.MagicState.HasValue)
|
||||
{
|
||||
query = filter.MagicState.Value switch
|
||||
{
|
||||
1 => query.Where(x => x.MagicStartedAt != null && x.MagicCompletedAt == null),
|
||||
2 => query.Where(x => x.MagicCompletedAt != null),
|
||||
_ => query.Where(x => x.MagicStartedAt == null && x.MagicCompletedAt == null)
|
||||
};
|
||||
}
|
||||
|
||||
if (filter.PackagePurchasedFrom.HasValue)
|
||||
query = query.Where(x => x.PackagePurchasedAt >= filter.PackagePurchasedFrom.Value);
|
||||
|
||||
if (filter.PackagePurchasedTo.HasValue)
|
||||
query = query.Where(x => x.PackagePurchasedAt <= filter.PackagePurchasedTo.Value);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.ClubMembershipCycleCQ.Queries.GetAllClubMembershipCycleByFilter;
|
||||
|
||||
public record GetAllClubMembershipCycleByFilterQuery : IRequest<GetAllClubMembershipCycleByFilterResponseDto>
|
||||
{
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
public string? SortBy { get; init; }
|
||||
public GetAllClubMembershipCycleByFilterFilter? Filter { get; init; }
|
||||
}
|
||||
|
||||
public class GetAllClubMembershipCycleByFilterFilter
|
||||
{
|
||||
public long? UserId { get; set; }
|
||||
public bool? IsCurrentCycle { get; set; }
|
||||
public long? PackageId { get; set; }
|
||||
public int? CycleNumber { get; set; }
|
||||
public PackagePurchaseMethod? PurchaseMethod { get; set; }
|
||||
/// <summary>0=none, 1=active magic, 2=completed magic</summary>
|
||||
public int? MagicState { get; set; }
|
||||
public DateTime? PackagePurchasedFrom { get; set; }
|
||||
public DateTime? PackagePurchasedTo { get; set; }
|
||||
}
|
||||
|
||||
public class GetAllClubMembershipCycleByFilterResponseDto
|
||||
{
|
||||
public MetaData MetaData { get; set; }
|
||||
public List<GetAllClubMembershipCycleByFilterResponseModel>? Models { get; set; }
|
||||
}
|
||||
|
||||
public class GetAllClubMembershipCycleByFilterResponseModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public long ClubMembershipId { get; set; }
|
||||
public int CycleNumber { get; set; }
|
||||
public long PackageId { get; set; }
|
||||
public string PackageName { get; set; } = string.Empty;
|
||||
public long PackageAmount { get; set; }
|
||||
public PackagePurchaseMethod PurchaseMethod { get; set; }
|
||||
public DateTime PackagePurchasedAt { get; set; }
|
||||
public DateTime? MagicStartedAt { get; set; }
|
||||
public DateTime? MagicCompletedAt { get; set; }
|
||||
public bool IsCurrentCycle { get; set; }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
namespace CMSMicroservice.Application.ClubMembershipCycleCQ.Queries.GetAllClubMembershipCycleByFilter;
|
||||
|
||||
public class GetAllClubMembershipCycleByFilterQueryHandler
|
||||
: IRequestHandler<GetAllClubMembershipCycleByFilterQuery, GetAllClubMembershipCycleByFilterResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetAllClubMembershipCycleByFilterQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetAllClubMembershipCycleByFilterResponseDto> Handle(
|
||||
GetAllClubMembershipCycleByFilterQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.ClubMembershipCycles
|
||||
.Include(x => x.User)
|
||||
.Include(x => x.Package)
|
||||
.AsNoTracking()
|
||||
.ApplyClubMembershipCycleFilter(request.Filter)
|
||||
.ApplyOrder(sortBy: request.SortBy ?? "-PackagePurchasedAt");
|
||||
|
||||
return new GetAllClubMembershipCycleByFilterResponseDto
|
||||
{
|
||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||
Models = await query
|
||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.Select(x => new GetAllClubMembershipCycleByFilterResponseModel
|
||||
{
|
||||
Id = x.Id,
|
||||
UserId = x.UserId,
|
||||
UserName = x.User != null ? $"{x.User.FirstName} {x.User.LastName}" : string.Empty,
|
||||
ClubMembershipId = x.ClubMembershipId,
|
||||
CycleNumber = x.CycleNumber,
|
||||
PackageId = x.PackageId,
|
||||
PackageName = x.Package != null ? x.Package.Title : string.Empty,
|
||||
PackageAmount = x.PackageAmount,
|
||||
PurchaseMethod = x.PurchaseMethod,
|
||||
PackagePurchasedAt = x.PackagePurchasedAt,
|
||||
MagicStartedAt = x.MagicStartedAt,
|
||||
MagicCompletedAt = x.MagicCompletedAt,
|
||||
IsCurrentCycle = x.IsCurrentCycle,
|
||||
Created = x.Created
|
||||
})
|
||||
.ToListAsync(cancellationToken)
|
||||
};
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
namespace CMSMicroservice.Application.ClubMembershipCycleCQ.Queries.GetClubMembershipCycleSummary;
|
||||
|
||||
public record GetClubMembershipCycleSummaryQuery : IRequest<GetClubMembershipCycleSummaryResponseDto>
|
||||
{
|
||||
public GetAllClubMembershipCycleByFilter.GetAllClubMembershipCycleByFilterFilter? Filter { get; init; }
|
||||
}
|
||||
|
||||
public class GetClubMembershipCycleSummaryResponseDto
|
||||
{
|
||||
public long TotalCount { get; set; }
|
||||
public long CurrentCycleCount { get; set; }
|
||||
public long MagicActiveCount { get; set; }
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
using CMSMicroservice.Application.ClubMembershipCycleCQ.Queries.GetAllClubMembershipCycleByFilter;
|
||||
|
||||
namespace CMSMicroservice.Application.ClubMembershipCycleCQ.Queries.GetClubMembershipCycleSummary;
|
||||
|
||||
public class GetClubMembershipCycleSummaryQueryHandler
|
||||
: IRequestHandler<GetClubMembershipCycleSummaryQuery, GetClubMembershipCycleSummaryResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetClubMembershipCycleSummaryQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetClubMembershipCycleSummaryResponseDto> Handle(
|
||||
GetClubMembershipCycleSummaryQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.ClubMembershipCycles
|
||||
.AsNoTracking()
|
||||
.ApplyClubMembershipCycleFilter(request.Filter);
|
||||
|
||||
var totalCount = await query.LongCountAsync(cancellationToken);
|
||||
var currentCycleCount = await query.LongCountAsync(x => x.IsCurrentCycle, cancellationToken);
|
||||
var magicActiveCount = await query.LongCountAsync(
|
||||
x => x.MagicStartedAt != null && x.MagicCompletedAt == null,
|
||||
cancellationToken);
|
||||
|
||||
return new GetClubMembershipCycleSummaryResponseDto
|
||||
{
|
||||
TotalCount = totalCount,
|
||||
CurrentCycleCount = currentCycleCount,
|
||||
MagicActiveCount = magicActiveCount
|
||||
};
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
using CMSMicroservice.Domain.Entities.History;
|
||||
|
||||
namespace CMSMicroservice.Application.ClubMembershipCycleHistoryCQ.Queries.GetAllClubMembershipCycleHistoryByFilter;
|
||||
|
||||
public static class ClubMembershipCycleHistoryFilterExtensions
|
||||
{
|
||||
public static IQueryable<ClubMembershipCycleHistory> ApplyClubMembershipCycleHistoryFilter(
|
||||
this IQueryable<ClubMembershipCycleHistory> query,
|
||||
GetAllClubMembershipCycleHistoryByFilterFilter? filter)
|
||||
{
|
||||
if (filter == null) return query;
|
||||
|
||||
if (filter.UserId.HasValue)
|
||||
query = query.Where(x => x.UserId == filter.UserId.Value);
|
||||
|
||||
if (filter.ClubMembershipCycleId.HasValue)
|
||||
query = query.Where(x => x.ClubMembershipCycleId == filter.ClubMembershipCycleId.Value);
|
||||
|
||||
if (filter.CycleNumber.HasValue)
|
||||
query = query.Where(x => x.CycleNumber == filter.CycleNumber.Value);
|
||||
|
||||
if (filter.Action.HasValue)
|
||||
query = query.Where(x => x.Action == filter.Action.Value);
|
||||
|
||||
if (filter.CreatedFrom.HasValue)
|
||||
query = query.Where(x => x.Created >= filter.CreatedFrom.Value);
|
||||
|
||||
if (filter.CreatedTo.HasValue)
|
||||
query = query.Where(x => x.Created <= filter.CreatedTo.Value);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.ClubMembershipCycleHistoryCQ.Queries.GetAllClubMembershipCycleHistoryByFilter;
|
||||
|
||||
public record GetAllClubMembershipCycleHistoryByFilterQuery : IRequest<GetAllClubMembershipCycleHistoryByFilterResponseDto>
|
||||
{
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
public string? SortBy { get; init; }
|
||||
public GetAllClubMembershipCycleHistoryByFilterFilter? Filter { get; init; }
|
||||
}
|
||||
|
||||
public class GetAllClubMembershipCycleHistoryByFilterFilter
|
||||
{
|
||||
public long? UserId { get; set; }
|
||||
public long? ClubMembershipCycleId { get; set; }
|
||||
public int? CycleNumber { get; set; }
|
||||
public ClubMembershipCycleAction? Action { get; set; }
|
||||
public DateTime? CreatedFrom { get; set; }
|
||||
public DateTime? CreatedTo { get; set; }
|
||||
}
|
||||
|
||||
public class GetAllClubMembershipCycleHistoryByFilterResponseDto
|
||||
{
|
||||
public MetaData MetaData { get; set; }
|
||||
public List<GetAllClubMembershipCycleHistoryByFilterResponseModel>? Models { get; set; }
|
||||
}
|
||||
|
||||
public class GetAllClubMembershipCycleHistoryByFilterResponseModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long ClubMembershipCycleId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public int CycleNumber { get; set; }
|
||||
public bool OldIsCurrentCycle { get; set; }
|
||||
public bool NewIsCurrentCycle { get; set; }
|
||||
public DateTime? OldMagicStartedAt { get; set; }
|
||||
public DateTime? NewMagicStartedAt { get; set; }
|
||||
public DateTime? OldMagicCompletedAt { get; set; }
|
||||
public DateTime? NewMagicCompletedAt { get; set; }
|
||||
public ClubMembershipCycleAction Action { get; set; }
|
||||
public string? PerformedBy { get; set; }
|
||||
public string? Reason { get; set; }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
namespace CMSMicroservice.Application.ClubMembershipCycleHistoryCQ.Queries.GetAllClubMembershipCycleHistoryByFilter;
|
||||
|
||||
public class GetAllClubMembershipCycleHistoryByFilterQueryHandler
|
||||
: IRequestHandler<GetAllClubMembershipCycleHistoryByFilterQuery, GetAllClubMembershipCycleHistoryByFilterResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetAllClubMembershipCycleHistoryByFilterQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetAllClubMembershipCycleHistoryByFilterResponseDto> Handle(
|
||||
GetAllClubMembershipCycleHistoryByFilterQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.ClubMembershipCycleHistories
|
||||
.Include(x => x.ClubMembershipCycle)
|
||||
.ThenInclude(c => c!.User)
|
||||
.AsNoTracking()
|
||||
.ApplyClubMembershipCycleHistoryFilter(request.Filter)
|
||||
.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
||||
|
||||
return new GetAllClubMembershipCycleHistoryByFilterResponseDto
|
||||
{
|
||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||
Models = await query
|
||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.Select(x => new GetAllClubMembershipCycleHistoryByFilterResponseModel
|
||||
{
|
||||
Id = x.Id,
|
||||
ClubMembershipCycleId = x.ClubMembershipCycleId,
|
||||
UserId = x.UserId,
|
||||
UserName = x.ClubMembershipCycle != null && x.ClubMembershipCycle.User != null
|
||||
? $"{x.ClubMembershipCycle.User.FirstName} {x.ClubMembershipCycle.User.LastName}"
|
||||
: string.Empty,
|
||||
CycleNumber = x.CycleNumber,
|
||||
OldIsCurrentCycle = x.OldIsCurrentCycle,
|
||||
NewIsCurrentCycle = x.NewIsCurrentCycle,
|
||||
OldMagicStartedAt = x.OldMagicStartedAt,
|
||||
NewMagicStartedAt = x.NewMagicStartedAt,
|
||||
OldMagicCompletedAt = x.OldMagicCompletedAt,
|
||||
NewMagicCompletedAt = x.NewMagicCompletedAt,
|
||||
Action = x.Action,
|
||||
PerformedBy = x.PerformedBy,
|
||||
Reason = x.Reason,
|
||||
Created = x.Created
|
||||
})
|
||||
.ToListAsync(cancellationToken)
|
||||
};
|
||||
}
|
||||
}
|
||||
+3
-40
@@ -1,5 +1,4 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Entities.Commission;
|
||||
using CMSMicroservice.Domain.Entities.History;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
@@ -25,7 +24,6 @@ public class BulkCreditPayoutsCommandHandler : IRequestHandler<BulkCreditPayouts
|
||||
|
||||
public async Task<BulkCreditPayoutsResult> Handle(BulkCreditPayoutsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// بارگذاری همه پرداختهای Pending این هفته
|
||||
var pendingPayouts = await _context.UserCommissionPayouts
|
||||
.Where(p => p.WeekDefinitionId == request.WeekDefinitionId
|
||||
&& p.Status == CommissionPayoutStatus.Pending
|
||||
@@ -37,72 +35,37 @@ public class BulkCreditPayoutsCommandHandler : IRequestHandler<BulkCreditPayouts
|
||||
return new BulkCreditPayoutsResult { CreditedCount = 0, TotalCredited = 0 };
|
||||
}
|
||||
|
||||
// بارگذاری کیف پول همه کاربران مربوطه (یک query)
|
||||
var userIds = pendingPayouts.Select(p => p.UserId).Distinct().ToList();
|
||||
var wallets = await _context.UserWallets
|
||||
.Where(w => userIds.Contains(w.UserId) && !w.IsDeleted)
|
||||
.ToDictionaryAsync(w => w.UserId, cancellationToken);
|
||||
|
||||
var performedBy = _currentUser.GetPerformedBy() ?? "System";
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var walletHistories = new List<UserWalletHistory>();
|
||||
var payoutHistories = new List<CommissionPayoutHistory>();
|
||||
var creditedCount = 0;
|
||||
long totalCredited = 0;
|
||||
|
||||
foreach (var payout in pendingPayouts)
|
||||
{
|
||||
if (!wallets.TryGetValue(payout.UserId, out var wallet))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Wallet not found for UserId={UserId}, PayoutId={PayoutId} — skipping",
|
||||
payout.UserId, payout.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
var oldBalance = wallet.Balance;
|
||||
|
||||
// واریز مبلغ به کیف پول
|
||||
wallet.Balance += payout.TotalAmount;
|
||||
|
||||
walletHistories.Add(new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
ChangeValue = payout.TotalAmount,
|
||||
ChangeNerworkValue = 0,
|
||||
ChangeDiscountValue = 0,
|
||||
IsIncrease = true,
|
||||
RefrenceId = payout.Id
|
||||
});
|
||||
|
||||
// بهروزرسانی وضعیت پرداخت
|
||||
payout.Status = CommissionPayoutStatus.Paid;
|
||||
payout.PaidAt = now;
|
||||
payout.LastModified = now;
|
||||
|
||||
payoutHistories.Add(new CommissionPayoutHistory
|
||||
{
|
||||
UserCommissionPayoutId = payout.Id,
|
||||
UserCommissionPayoutId = payout.Id,
|
||||
UserId = payout.UserId,
|
||||
WeekDefinitionId = payout.WeekDefinitionId,
|
||||
AmountBefore = 0,
|
||||
AmountBefore = payout.TotalAmount,
|
||||
AmountAfter = payout.TotalAmount,
|
||||
OldStatus = CommissionPayoutStatus.Pending,
|
||||
NewStatus = CommissionPayoutStatus.Paid,
|
||||
Action = CommissionPayoutAction.Paid,
|
||||
PerformedBy = performedBy,
|
||||
Reason = $"واریز دستهجمعی کمیسیون هفته {payout.WeekDefinitionId} توسط {performedBy}"
|
||||
Reason = $"تایید پرداخت دستهجمعی کمیسیون هفته {payout.WeekDefinitionId} توسط {performedBy}"
|
||||
});
|
||||
|
||||
creditedCount++;
|
||||
totalCredited += payout.TotalAmount;
|
||||
}
|
||||
|
||||
await _context.UserWalletHistories.AddRangeAsync(walletHistories, cancellationToken);
|
||||
await _context.CommissionPayoutHistories.AddRangeAsync(payoutHistories, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
|
||||
+6
-1
@@ -1,3 +1,5 @@
|
||||
using CMSMicroservice.Application.Common;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Commands.RequestWithdrawal;
|
||||
|
||||
public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawalCommand, Unit>
|
||||
@@ -37,7 +39,10 @@ public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawal
|
||||
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.Common;
|
||||
|
||||
public static class BlogSlugHelper
|
||||
{
|
||||
private const int MaxSlugLength = 200;
|
||||
|
||||
public static string? NormalizeOptional(string? slug)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(slug))
|
||||
return null;
|
||||
|
||||
return slug.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a URL slug from title (ASCII letters/digits). Persian-only titles yield empty string.
|
||||
/// </summary>
|
||||
public static string GenerateFromTitle(string title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
return string.Empty;
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
var lastWasHyphen = false;
|
||||
|
||||
foreach (var c in title.Trim().ToLowerInvariant())
|
||||
{
|
||||
if (c is >= 'a' and <= 'z' or >= '0' and <= '9')
|
||||
{
|
||||
sb.Append(c);
|
||||
lastWasHyphen = false;
|
||||
}
|
||||
else if (c is ' ' or '_' or '-')
|
||||
{
|
||||
if (sb.Length > 0 && !lastWasHyphen)
|
||||
{
|
||||
sb.Append('-');
|
||||
lastWasHyphen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result = sb.ToString().Trim('-');
|
||||
return result.Length > MaxSlugLength ? result[..MaxSlugLength].TrimEnd('-') : result;
|
||||
}
|
||||
|
||||
public static string CreateFallbackSlug() =>
|
||||
$"post-{Guid.NewGuid():N}"[..Math.Min(20, MaxSlugLength)];
|
||||
|
||||
public static string ResolveForCreate(string? requestedSlug, string title)
|
||||
{
|
||||
var normalized = NormalizeOptional(requestedSlug);
|
||||
if (!string.IsNullOrEmpty(normalized))
|
||||
return normalized;
|
||||
|
||||
var fromTitle = GenerateFromTitle(title);
|
||||
return !string.IsNullOrEmpty(fromTitle) ? fromTitle : CreateFallbackSlug();
|
||||
}
|
||||
|
||||
public static async Task<string> EnsureUniquePostSlugAsync(
|
||||
IApplicationDbContext context,
|
||||
string slug,
|
||||
long? excludePostId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var candidate = slug;
|
||||
var suffix = 2;
|
||||
|
||||
while (await context.BlogPosts.AnyAsync(
|
||||
p => p.Slug == candidate && !p.IsDeleted && (excludePostId == null || p.Id != excludePostId.Value),
|
||||
cancellationToken))
|
||||
{
|
||||
var suffixStr = $"-{suffix}";
|
||||
var maxBase = MaxSlugLength - suffixStr.Length;
|
||||
var basePart = slug.Length > maxBase ? slug[..maxBase].TrimEnd('-') : slug;
|
||||
candidate = basePart + suffixStr;
|
||||
suffix++;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Linq.Dynamic.Core;
|
||||
using CMSMicroservice.Domain.Common;
|
||||
|
||||
@@ -8,16 +9,46 @@ public static class SortByExtensions
|
||||
public static IQueryable<TSource> ApplyOrder<TSource>(this IQueryable<TSource> source,
|
||||
string? sortBy) where TSource : BaseAuditableEntity
|
||||
{
|
||||
// default sort approach
|
||||
if (sortBy is null or "")
|
||||
{
|
||||
source = source.OrderByDescending(p => p.Created);
|
||||
return source;
|
||||
return source.OrderByDescending(p => p.Created);
|
||||
}
|
||||
|
||||
// sort using dynamic linq
|
||||
source = source.OrderBy(sortBy);
|
||||
return source.OrderBy(NormalizeSortBy(sortBy));
|
||||
}
|
||||
|
||||
return source;
|
||||
/// <summary>
|
||||
/// موجودیدارها اول، سپس سورت درخواستی کاربر (یا Created desc اگر SortBy خالی باشد).
|
||||
/// </summary>
|
||||
public static IQueryable<TSource> ApplyOrderWithInStockPriority<TSource>(
|
||||
this IQueryable<TSource> source,
|
||||
string? sortBy,
|
||||
Expression<Func<TSource, bool>> isInStock) where TSource : BaseAuditableEntity
|
||||
{
|
||||
var ordered = source.OrderByDescending(isInStock);
|
||||
if (string.IsNullOrWhiteSpace(sortBy))
|
||||
return ordered.ThenByDescending(p => p.Created);
|
||||
|
||||
return ordered.ThenBy(NormalizeSortBy(sortBy));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dynamic LINQ treats "-Created" as unary minus on DateTime (invalid).
|
||||
/// Project convention: leading '-' means descending sort.
|
||||
/// </summary>
|
||||
private static string NormalizeSortBy(string sortBy)
|
||||
{
|
||||
sortBy = sortBy.Trim();
|
||||
if (sortBy.StartsWith('-') && sortBy.Length > 1)
|
||||
{
|
||||
return $"{sortBy[1..]} descending";
|
||||
}
|
||||
|
||||
if (sortBy.StartsWith('+') && sortBy.Length > 1)
|
||||
{
|
||||
return $"{sortBy[1..]} ascending";
|
||||
}
|
||||
|
||||
return sortBy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using CMSMicroservice.Domain.Entities.Club;
|
||||
using CMSMicroservice.Domain.Entities.History;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.Common.Helpers;
|
||||
|
||||
public static class ClubMembershipCycleHistoryWriter
|
||||
{
|
||||
public static void WriteCycleStarted(
|
||||
IApplicationDbContext context,
|
||||
ClubMembershipCycle cycle,
|
||||
string? performedBy = "System",
|
||||
string? reason = null)
|
||||
{
|
||||
context.ClubMembershipCycleHistories.Add(new ClubMembershipCycleHistory
|
||||
{
|
||||
ClubMembershipCycleId = cycle.Id,
|
||||
UserId = cycle.UserId,
|
||||
CycleNumber = cycle.CycleNumber,
|
||||
OldIsCurrentCycle = false,
|
||||
NewIsCurrentCycle = cycle.IsCurrentCycle,
|
||||
OldMagicStartedAt = null,
|
||||
NewMagicStartedAt = cycle.MagicStartedAt,
|
||||
OldMagicCompletedAt = null,
|
||||
NewMagicCompletedAt = cycle.MagicCompletedAt,
|
||||
Action = ClubMembershipCycleAction.Started,
|
||||
PerformedBy = performedBy,
|
||||
Reason = reason
|
||||
});
|
||||
}
|
||||
|
||||
public static void WriteCycleClosed(
|
||||
IApplicationDbContext context,
|
||||
ClubMembershipCycle cycle,
|
||||
string? performedBy = "System",
|
||||
string? reason = null)
|
||||
{
|
||||
context.ClubMembershipCycleHistories.Add(new ClubMembershipCycleHistory
|
||||
{
|
||||
ClubMembershipCycleId = cycle.Id,
|
||||
UserId = cycle.UserId,
|
||||
CycleNumber = cycle.CycleNumber,
|
||||
OldIsCurrentCycle = true,
|
||||
NewIsCurrentCycle = false,
|
||||
OldMagicStartedAt = cycle.MagicStartedAt,
|
||||
NewMagicStartedAt = cycle.MagicStartedAt,
|
||||
OldMagicCompletedAt = cycle.MagicCompletedAt,
|
||||
NewMagicCompletedAt = cycle.MagicCompletedAt,
|
||||
Action = ClubMembershipCycleAction.Completed,
|
||||
PerformedBy = performedBy,
|
||||
Reason = reason ?? "دوره جدید جایگزین شد"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<ProductImage> ProductImages { get; }
|
||||
DbSet<User> Users { get; }
|
||||
DbSet<UserInvitation> UserInvitations { get; }
|
||||
DbSet<OtpToken> OtpTokens { get; }
|
||||
DbSet<Contract> Contracts { 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 بازگشت بعد از پرداخت
|
||||
/// </summary>
|
||||
public string CallbackUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// نام پرداختکننده (برای درگاههایی مثل ۲۰کارت)
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// شماره فاکتور / سفارش
|
||||
/// </summary>
|
||||
public string Invoice { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// ایمیل (اختیاری — برای ۲۰کارت)
|
||||
/// </summary>
|
||||
public string? Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// کد ملی (اختیاری — برای ۲۰کارت)
|
||||
/// </summary>
|
||||
public string? NationalId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(request.AdminNotes))
|
||||
{
|
||||
order.DeliveryDescription = request.AdminNotes;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new UpdateOrderStatusResponseDto
|
||||
|
||||
+42
-18
@@ -1,3 +1,4 @@
|
||||
using CMSMicroservice.Application.Common;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using MediatR;
|
||||
@@ -80,35 +81,58 @@ public class GetAllDiscountOrdersQueryHandler : IRequestHandler<GetAllDiscountOr
|
||||
// Apply pagination
|
||||
var pagination = request.PaginationQuery ?? new PaginationState { PageNumber = 1, PageSize = 20 };
|
||||
|
||||
var orders = await query
|
||||
var rows = await query
|
||||
.OrderByDescending(o => o.Created)
|
||||
.Skip((pagination.PageNumber - 1) * pagination.PageSize)
|
||||
.Take(pagination.PageSize)
|
||||
.Select(o => new AdminOrderDto
|
||||
.Select(o => new
|
||||
{
|
||||
Id = o.Id,
|
||||
UserId = o.UserId,
|
||||
UserFullName = (o.User.FirstName ?? "") + " " + (o.User.LastName ?? ""),
|
||||
UserMobile = o.User.Mobile,
|
||||
TotalAmount = o.TotalAmount,
|
||||
DiscountBalanceUsed = o.DiscountBalanceUsed,
|
||||
GatewayAmountPaid = o.GatewayAmountPaid,
|
||||
VatAmount = o.VatAmount,
|
||||
PaymentStatus = o.PaymentStatus,
|
||||
PaymentDate = o.PaymentDate,
|
||||
DeliveryStatus = o.DeliveryStatus,
|
||||
DeliveryDate = null, // TODO: Add DeliveryDate to DiscountOrder if needed
|
||||
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,
|
||||
ReceiverMobile = o.User.Mobile,
|
||||
TrackingCode = o.TrackingCode,
|
||||
o.TrackingCode,
|
||||
AdminNote = o.DeliveryDescription,
|
||||
Created = o.Created,
|
||||
LastModified = o.LastModified,
|
||||
o.Created,
|
||||
o.LastModified,
|
||||
ItemsCount = o.OrderDetails.Count
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var orders = rows.Select(o => new AdminOrderDto
|
||||
{
|
||||
Id = o.Id,
|
||||
UserId = o.UserId,
|
||||
UserFullName = UserDisplayName.From(o.FirstName, o.LastName, o.Mobile, o.UserId),
|
||||
UserMobile = o.Mobile,
|
||||
TotalAmount = o.TotalAmount,
|
||||
DiscountBalanceUsed = o.DiscountBalanceUsed,
|
||||
GatewayAmountPaid = o.GatewayAmountPaid,
|
||||
VatAmount = o.VatAmount,
|
||||
PaymentStatus = o.PaymentStatus,
|
||||
PaymentDate = o.PaymentDate,
|
||||
DeliveryStatus = o.DeliveryStatus,
|
||||
DeliveryDate = null,
|
||||
ShippingAddress = o.ShippingAddress,
|
||||
ReceiverName = o.ReceiverName,
|
||||
ReceiverMobile = o.Mobile,
|
||||
TrackingCode = o.TrackingCode,
|
||||
AdminNote = o.AdminNote,
|
||||
Created = o.Created,
|
||||
LastModified = o.LastModified,
|
||||
ItemsCount = o.ItemsCount
|
||||
}).ToList();
|
||||
|
||||
return new GetAllDiscountOrdersResponseDto
|
||||
{
|
||||
MetaData = new MetaData
|
||||
|
||||
+4
@@ -8,6 +8,10 @@ public class GetDiscountProductsQuery : IRequest<GetDiscountProductsResponseDto>
|
||||
public PaginationState? PaginationQuery { get; set; }
|
||||
public long? CategoryId { get; set; }
|
||||
public string? SearchTerm { get; set; }
|
||||
/// <summary>
|
||||
/// true = فقط موجود (RemainingCount > 0)، false = فقط ناموجود
|
||||
/// </summary>
|
||||
public bool? InStock { get; set; }
|
||||
public bool? IsActive { get; set; }
|
||||
public int? MinPrice { get; set; }
|
||||
public int? MaxPrice { get; set; }
|
||||
|
||||
+7
-4
@@ -42,6 +42,11 @@ public class GetDiscountProductsQueryHandler : IRequestHandler<GetDiscountProduc
|
||||
query = query.Where(p => p.IsActive == request.IsActive.Value);
|
||||
}
|
||||
|
||||
if (request.InStock == true)
|
||||
query = query.Where(p => p.RemainingCount > 0);
|
||||
else if (request.InStock == false)
|
||||
query = query.Where(p => p.RemainingCount <= 0);
|
||||
|
||||
if (request.MinPrice.HasValue)
|
||||
{
|
||||
query = query.Where(p => p.Price >= request.MinPrice.Value);
|
||||
@@ -52,10 +57,8 @@ public class GetDiscountProductsQueryHandler : IRequestHandler<GetDiscountProduc
|
||||
query = query.Where(p => p.Price <= request.MaxPrice.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(request.SortBy))
|
||||
query = query.ApplyOrder(request.SortBy);
|
||||
else
|
||||
query = query.OrderByDescending(p => p.Created);
|
||||
// موجودیدارها اول، سپس سورت درخواستی
|
||||
query = query.ApplyOrderWithInStockPriority(request.SortBy, p => p.RemainingCount > 0);
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
|
||||
+2
@@ -32,6 +32,8 @@ public class UserAddressDto
|
||||
public string Title { get; set; }
|
||||
public string Address { get; set; }
|
||||
public string PostalCode { get; set; }
|
||||
/// <summary>شماره تماس سفارشدهنده (موبایل کاربر) برای برچسب پست</summary>
|
||||
public string? Phone { get; set; }
|
||||
}
|
||||
|
||||
public class OrderItemDto
|
||||
|
||||
+3
-1
@@ -25,6 +25,7 @@ public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, Order
|
||||
|
||||
var order = await query
|
||||
.Include(o => o.UserAddress)
|
||||
.Include(o => o.User)
|
||||
.Include(o => o.OrderDetails)
|
||||
.ThenInclude(od => od.Product)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -50,7 +51,8 @@ public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, Order
|
||||
{
|
||||
Title = order.UserAddress.Title,
|
||||
Address = order.UserAddress.Address,
|
||||
PostalCode = order.UserAddress.PostalCode
|
||||
PostalCode = order.UserAddress.PostalCode,
|
||||
Phone = order.User?.Mobile
|
||||
},
|
||||
Items = order.OrderDetails.Select(od => new OrderItemDto
|
||||
{
|
||||
|
||||
+12
-2
@@ -150,8 +150,18 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
|
||||
|
||||
await _context.UserWalletHistories.AddAsync(walletLog, cancellationToken);
|
||||
|
||||
// 9. تنظیم روش خرید پکیج
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
||||
// 9. تنظیم روش خرید پکیج + ثبت ledger خرید دستی
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.Manual;
|
||||
|
||||
_context.UserPackagePurchases.Add(new UserPackagePurchase
|
||||
{
|
||||
UserId = request.UserId,
|
||||
PackageId = package.Id,
|
||||
PurchaseMethod = PackagePurchaseMethod.Manual,
|
||||
PurchasedAt = DateTime.Now,
|
||||
Amount = balanceAmount,
|
||||
TransactionId = transaction.Id
|
||||
});
|
||||
|
||||
// 10. ذخیره همه تغییرات
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ public class ProcessManualMembershipPaymentCommandHandler : IRequestHandler<Proc
|
||||
};
|
||||
await _context.UserWalletHistories.AddAsync(balanceLog, cancellationToken);
|
||||
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.Manual;
|
||||
// 10. بهروزرسانی ManualPayment با TransactionId
|
||||
manualPayment.TransactionId = transaction.Id;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
+106
-8
@@ -1,4 +1,5 @@
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStatistics;
|
||||
|
||||
@@ -34,17 +35,28 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
|
||||
}
|
||||
var allDescendants = GetAllDescendants(userId, allUsers);
|
||||
|
||||
// Statistics for the user's network (all descendants)
|
||||
var totalMembers = allDescendants.Count;
|
||||
var activeMembers = allDescendants.Count(x => !x.IsDeleted);
|
||||
|
||||
// Get direct left and right children
|
||||
var leftChild = allUsers.FirstOrDefault(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Left);
|
||||
var rightChild = allUsers.FirstOrDefault(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Right);
|
||||
|
||||
// Count all descendants in left and right subtrees
|
||||
var leftLegCount = leftChild != null ? GetAllDescendants(leftChild.Id, allUsers).Count + 1 : 0; // +1 for leftChild itself
|
||||
var rightLegCount = rightChild != null ? GetAllDescendants(rightChild.Id, allUsers).Count + 1 : 0; // +1 for rightChild itself
|
||||
var leftLegUserIds = leftChild != null ? GetSubtreeUserIds(leftChild.Id, allUsers) : new HashSet<long>();
|
||||
var rightLegUserIds = rightChild != null ? GetSubtreeUserIds(rightChild.Id, allUsers) : new HashSet<long>();
|
||||
var allLegUserIds = leftLegUserIds.Union(rightLegUserIds).ToList();
|
||||
|
||||
// سازمان چپ/راست = فقط اعضای فعال باشگاه در هر پا
|
||||
var activeClubUserIds = allLegUserIds.Count == 0
|
||||
? new HashSet<long>()
|
||||
: (await _context.ClubMemberships
|
||||
.AsNoTracking()
|
||||
.Where(cm => cm.IsActive && !cm.IsDeleted && allLegUserIds.Contains(cm.UserId))
|
||||
.Select(cm => cm.UserId)
|
||||
.ToListAsync(cancellationToken))
|
||||
.ToHashSet();
|
||||
|
||||
var leftLegCount = leftLegUserIds.Count(id => activeClubUserIds.Contains(id));
|
||||
var rightLegCount = rightLegUserIds.Count(id => activeClubUserIds.Contains(id));
|
||||
var totalMembers = leftLegCount + rightLegCount;
|
||||
var activeMembers = totalMembers;
|
||||
|
||||
double leftPercentage = totalMembers > 0 ? (leftLegCount / (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
|
||||
}).ToList();
|
||||
|
||||
var (goldLeft, goldRight, silverLeft, silverRight) =
|
||||
await LoadPackageHolderCountsByLegAsync(leftLegUserIds, rightLegUserIds, cancellationToken);
|
||||
|
||||
return new GetNetworkStatisticsResponseDto
|
||||
{
|
||||
TotalMembers = totalMembers,
|
||||
@@ -136,10 +151,93 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
|
||||
MaxDepth = maxDepth,
|
||||
LevelDistribution = levelDistribution,
|
||||
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>
|
||||
/// Recursively get all descendants of a user
|
||||
/// </summary>
|
||||
|
||||
+15
@@ -2,9 +2,13 @@ namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStat
|
||||
|
||||
public class GetNetworkStatisticsResponseDto
|
||||
{
|
||||
/// <summary>مجموع اعضای فعال باشگاه در پای چپ و راست (= LeftLegCount + RightLegCount)</summary>
|
||||
public int TotalMembers { get; set; }
|
||||
/// <summary>همان TotalMembers — اعضای فعال باشگاه در شبکه کاربر</summary>
|
||||
public int ActiveMembers { get; set; }
|
||||
/// <summary>تعداد اعضای فعال باشگاه در زیردرخت پای چپ</summary>
|
||||
public int LeftLegCount { get; set; }
|
||||
/// <summary>تعداد اعضای فعال باشگاه در زیردرخت پای راست</summary>
|
||||
public int RightLegCount { get; set; }
|
||||
public double LeftPercentage { get; set; }
|
||||
public double RightPercentage { get; set; }
|
||||
@@ -13,6 +17,17 @@ public class GetNetworkStatisticsResponseDto
|
||||
public List<LevelDistributionModel> LevelDistribution { get; set; } = new();
|
||||
public List<MonthlyGrowthModel> MonthlyGrowth { get; set; } = new();
|
||||
public List<TopNetworkUserModel> TopUsers { get; set; } = new();
|
||||
|
||||
/// <summary>تعداد اعضای فعال باشگاه با پکیج طلایی جاری در پای چپ</summary>
|
||||
public int GoldLeftLegTotal { get; set; }
|
||||
/// <summary>تعداد اعضای فعال باشگاه با پکیج طلایی جاری در پای راست</summary>
|
||||
public int GoldRightLegTotal { get; set; }
|
||||
/// <summary>تعداد اعضای فعال باشگاه با پکیج نقرهای جاری در پای چپ</summary>
|
||||
public int SilverLeftLegTotal { get; set; }
|
||||
/// <summary>تعداد اعضای فعال باشگاه با پکیج نقرهای جاری در پای راست</summary>
|
||||
public int SilverRightLegTotal { get; set; }
|
||||
|
||||
public string WeakerLeg => LeftLegCount <= RightLegCount ? "Left" : "Right";
|
||||
}
|
||||
|
||||
public class LevelDistributionModel
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
|
||||
|
||||
+12
@@ -60,6 +60,18 @@ public class NetworkTreeDto
|
||||
|
||||
public long? PackageId { get; set; }
|
||||
|
||||
/// <summary>تعداد اعضای فعال با پکیج طلایی جاری در پای چپ (stats bar)</summary>
|
||||
public int GoldLeftLegTotal { get; set; }
|
||||
|
||||
/// <summary>تعداد اعضای فعال با پکیج طلایی جاری در پای راست (stats bar)</summary>
|
||||
public int GoldRightLegTotal { get; set; }
|
||||
|
||||
/// <summary>تعداد اعضای فعال با پکیج نقرهای جاری در پای چپ (stats bar)</summary>
|
||||
public int SilverLeftLegTotal { get; set; }
|
||||
|
||||
/// <summary>تعداد اعضای فعال با پکیج نقرهای جاری در پای راست (stats bar)</summary>
|
||||
public int SilverRightLegTotal { get; set; }
|
||||
|
||||
public NetworkTreeDto? LeftChild { get; set; }
|
||||
public NetworkTreeDto? RightChild { get; set; }
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
|
||||
|
||||
/// <summary>
|
||||
/// Shared verify + wallet credit path for package IPG payments (callback UI and reconciliation job).
|
||||
/// </summary>
|
||||
public class VerifyUserPackagePurchasePaymentCommand : IRequest<VerifyUserPackagePurchasePaymentResult>
|
||||
{
|
||||
public long PurchaseId { get; set; }
|
||||
public string Authority { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = "OK";
|
||||
}
|
||||
|
||||
public class VerifyUserPackagePurchasePaymentResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public bool AlreadyPaid { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
public long TransactionId { get; init; }
|
||||
public string ReferenceCode { get; init; } = string.Empty;
|
||||
public long PackageId { get; init; }
|
||||
public string PackageName { get; init; } = string.Empty;
|
||||
public long AmountPaid { get; init; }
|
||||
public DateTime PurchasedAt { get; init; }
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
using System.Data;
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
|
||||
using CMSMicroservice.Application.Common;
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Entities.Payment;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
|
||||
|
||||
public class VerifyUserPackagePurchasePaymentCommandHandler
|
||||
: IRequestHandler<VerifyUserPackagePurchasePaymentCommand, VerifyUserPackagePurchasePaymentResult>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IPaymentGatewayResolver _paymentGatewayResolver;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ISender _sender;
|
||||
private readonly IUserPaymentLock _paymentLock;
|
||||
private readonly ILogger<VerifyUserPackagePurchasePaymentCommandHandler> _logger;
|
||||
|
||||
public VerifyUserPackagePurchasePaymentCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IPaymentGatewayResolver paymentGatewayResolver,
|
||||
IConfiguration configuration,
|
||||
ISender sender,
|
||||
IUserPaymentLock paymentLock,
|
||||
ILogger<VerifyUserPackagePurchasePaymentCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_paymentGatewayResolver = paymentGatewayResolver;
|
||||
_configuration = configuration;
|
||||
_sender = sender;
|
||||
_paymentLock = paymentLock;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<VerifyUserPackagePurchasePaymentResult> Handle(
|
||||
VerifyUserPackagePurchasePaymentCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var purchaseUserId = await _context.UserPackagePurchases
|
||||
.Where(p => p.Id == request.PurchaseId && !p.IsDeleted)
|
||||
.Select(p => (long?)p.UserId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (purchaseUserId is null or 0)
|
||||
throw new NotFoundException("سفارش یافت نشد");
|
||||
|
||||
var verifyKey = !string.IsNullOrEmpty(request.Authority)
|
||||
? request.Authority
|
||||
: request.PurchaseId.ToString();
|
||||
|
||||
return await _paymentLock.ExecuteAsync(
|
||||
PaymentLockScopes.Verify(purchaseUserId.Value, verifyKey),
|
||||
PaymentLockStrategy.WaitForRelease,
|
||||
ct => HandleCoreAsync(request, ct),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<VerifyUserPackagePurchasePaymentResult> HandleCoreAsync(
|
||||
VerifyUserPackagePurchasePaymentCommand request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var purchase = await _context.UserPackagePurchases
|
||||
.Include(p => p.Package)
|
||||
.Where(p => p.Id == request.PurchaseId && !p.IsDeleted)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (purchase == null)
|
||||
throw new NotFoundException("سفارش یافت نشد");
|
||||
|
||||
var transaction = purchase.TransactionId.HasValue
|
||||
? await _context.Transactions
|
||||
.FirstOrDefaultAsync(t => t.Id == purchase.TransactionId.Value, ct)
|
||||
: null;
|
||||
|
||||
var paymentTx = !string.IsNullOrEmpty(request.Authority)
|
||||
? await _context.PaymentTransactions
|
||||
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, ct)
|
||||
: null;
|
||||
|
||||
if (IsPaymentAlreadyCompleted(transaction, paymentTx))
|
||||
{
|
||||
return BuildResult(purchase, transaction, paymentTx, alreadyPaid: true);
|
||||
}
|
||||
|
||||
if (!string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (transaction != null && transaction.PaymentStatus != PaymentStatus.Success)
|
||||
{
|
||||
transaction.PaymentStatus = PaymentStatus.Reject;
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
return new VerifyUserPackagePurchasePaymentResult
|
||||
{
|
||||
Success = false,
|
||||
Message = "پرداخت توسط کاربر لغو شد",
|
||||
PackageId = purchase.PackageId,
|
||||
PackageName = purchase.Package?.Title ?? string.Empty,
|
||||
AmountPaid = purchase.Amount,
|
||||
PurchasedAt = purchase.PurchasedAt
|
||||
};
|
||||
}
|
||||
|
||||
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
|
||||
var gateway = _paymentGatewayResolver.Get(paymentTx?.GatewayProvider);
|
||||
var verifyResult = await gateway.VerifyPaymentAsync(
|
||||
request.Authority, request.Status, (decimal)amountInToman, ct);
|
||||
|
||||
if (!verifyResult.IsSuccess)
|
||||
{
|
||||
if (paymentTx != null)
|
||||
{
|
||||
paymentTx.PaymentStatus = false;
|
||||
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
|
||||
paymentTx.VerificationStatusMessage = verifyResult.Message;
|
||||
}
|
||||
|
||||
if (transaction != null)
|
||||
transaction.PaymentStatus = PaymentStatus.Reject;
|
||||
|
||||
await _context.SaveChangesAsync(ct);
|
||||
|
||||
return new VerifyUserPackagePurchasePaymentResult
|
||||
{
|
||||
Success = false,
|
||||
Message = verifyResult.Message ?? "خرید پکیج ناموفق بود",
|
||||
TransactionId = transaction?.Id ?? 0,
|
||||
PackageId = purchase.PackageId,
|
||||
PackageName = purchase.Package?.Title ?? string.Empty,
|
||||
AmountPaid = purchase.Amount,
|
||||
PurchasedAt = purchase.PurchasedAt
|
||||
};
|
||||
}
|
||||
|
||||
await using var dbTx = await _context.Database.BeginTransactionAsync(IsolationLevel.Serializable, ct);
|
||||
try
|
||||
{
|
||||
if (purchase.TransactionId.HasValue)
|
||||
{
|
||||
transaction = await _context.Transactions
|
||||
.FirstOrDefaultAsync(t => t.Id == purchase.TransactionId.Value, ct);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(request.Authority))
|
||||
{
|
||||
paymentTx = await _context.PaymentTransactions
|
||||
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, ct);
|
||||
}
|
||||
|
||||
if (IsPaymentAlreadyCompleted(transaction, paymentTx))
|
||||
{
|
||||
await dbTx.CommitAsync(ct);
|
||||
return BuildResult(purchase, transaction, paymentTx, alreadyPaid: true);
|
||||
}
|
||||
|
||||
if (transaction == null)
|
||||
throw new InvalidOperationException("تراکنش سفارش یافت نشد");
|
||||
|
||||
var alreadyCredited = await _context.UserWalletHistories
|
||||
.AnyAsync(h => h.RefrenceId == transaction.Id && !h.IsDeleted, ct);
|
||||
|
||||
if (alreadyCredited)
|
||||
{
|
||||
transaction.PaymentStatus = PaymentStatus.Success;
|
||||
transaction.PaymentDate ??= DateTime.UtcNow;
|
||||
UpdatePaymentTransaction(paymentTx, verifyResult, markSuccess: true);
|
||||
await _context.SaveChangesAsync(ct);
|
||||
await dbTx.CommitAsync(ct);
|
||||
return BuildResult(purchase, transaction, paymentTx, alreadyPaid: true);
|
||||
}
|
||||
|
||||
UpdatePaymentTransaction(paymentTx, verifyResult, markSuccess: true);
|
||||
|
||||
transaction.PaymentStatus = PaymentStatus.Success;
|
||||
transaction.PaymentDate = DateTime.UtcNow;
|
||||
transaction.RefId = verifyResult.RefId;
|
||||
|
||||
var wallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == purchase.UserId, ct);
|
||||
|
||||
if (wallet == null)
|
||||
{
|
||||
wallet = new UserWallet
|
||||
{
|
||||
UserId = purchase.UserId,
|
||||
Balance = 0,
|
||||
DiscountBalance = 0,
|
||||
NetworkBalance = 0
|
||||
};
|
||||
_context.UserWallets.Add(wallet);
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
var packagePrice = purchase.Package?.Price
|
||||
?? throw new InvalidOperationException("پکیج سفارش یافت نشد");
|
||||
var discountMultiplier = purchase.Package.DiscountMultiplier;
|
||||
var discountAmount = (long)(packagePrice * (double)discountMultiplier);
|
||||
|
||||
var isTwentyCard = string.Equals(
|
||||
paymentTx?.GatewayProvider, "20card", StringComparison.OrdinalIgnoreCase);
|
||||
long balanceCredit = purchase.Amount;
|
||||
if (isTwentyCard)
|
||||
{
|
||||
balanceCredit = _configuration.GetValue<long?>(
|
||||
$"TwentyCard:Packages:{purchase.PackageId}:WalletCreditToman") ?? 0;
|
||||
if (balanceCredit <= 0)
|
||||
throw new InvalidOperationException("WalletCredit ۲۰کارت برای این پکیج پیکربندی نشده است");
|
||||
}
|
||||
|
||||
wallet.Balance += balanceCredit;
|
||||
wallet.DiscountBalance += discountAmount;
|
||||
|
||||
_context.UserWalletHistories.Add(new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
ChangeValue = balanceCredit,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
ChangeDiscountValue = discountAmount,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id,
|
||||
PackageId = purchase.PackageId
|
||||
});
|
||||
|
||||
var user = await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == purchase.UserId, ct);
|
||||
if (user != null)
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
||||
|
||||
await _context.SaveChangesAsync(ct);
|
||||
await dbTx.CommitAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await dbTx.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _sender.Send(new ActivateClubMembershipCommand
|
||||
{
|
||||
UserId = purchase.UserId,
|
||||
ForceActivation = false
|
||||
}, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Auto club activation failed after package verify. UserId={UserId}, PurchaseId={PurchaseId}",
|
||||
purchase.UserId,
|
||||
purchase.Id);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Package purchase verified. PurchaseId={PurchaseId}, UserId={UserId}, Authority={Authority}",
|
||||
purchase.Id,
|
||||
purchase.UserId,
|
||||
request.Authority);
|
||||
|
||||
return BuildResult(
|
||||
purchase, transaction, paymentTx, alreadyPaid: false, verifyResult.RefId);
|
||||
}
|
||||
|
||||
private static bool IsPaymentAlreadyCompleted(Transaction? transaction, PaymentTransaction? paymentTx)
|
||||
{
|
||||
return transaction?.PaymentStatus == PaymentStatus.Success
|
||||
|| paymentTx?.PaymentStatus == true;
|
||||
}
|
||||
|
||||
private static void UpdatePaymentTransaction(
|
||||
PaymentTransaction? paymentTx,
|
||||
PaymentVerificationResult verifyResult,
|
||||
bool markSuccess)
|
||||
{
|
||||
if (paymentTx == null) return;
|
||||
|
||||
paymentTx.PaymentStatus = markSuccess && verifyResult.IsSuccess;
|
||||
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
|
||||
paymentTx.VerificationStatusMessage = verifyResult.Message;
|
||||
paymentTx.CardPan = verifyResult.CardPan;
|
||||
paymentTx.CardHash = verifyResult.CardHash;
|
||||
paymentTx.RefId = verifyResult.TrackingCode;
|
||||
}
|
||||
|
||||
private static VerifyUserPackagePurchasePaymentResult BuildResult(
|
||||
UserPackagePurchase purchase,
|
||||
Transaction? transaction,
|
||||
PaymentTransaction? paymentTx,
|
||||
bool alreadyPaid,
|
||||
string? referenceCode = null)
|
||||
{
|
||||
var refCode = referenceCode
|
||||
?? paymentTx?.RefId
|
||||
?? transaction?.RefId
|
||||
?? string.Empty;
|
||||
|
||||
return new VerifyUserPackagePurchasePaymentResult
|
||||
{
|
||||
Success = true,
|
||||
AlreadyPaid = alreadyPaid,
|
||||
Message = alreadyPaid ? "پرداخت قبلاً تأیید شده بود" : "خرید پکیج با موفقیت تایید شد",
|
||||
TransactionId = transaction?.Id ?? 0,
|
||||
ReferenceCode = refCode,
|
||||
PackageId = purchase.PackageId,
|
||||
PackageName = purchase.Package?.Title ?? string.Empty,
|
||||
AmountPaid = purchase.Amount,
|
||||
PurchasedAt = purchase.PurchasedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
namespace CMSMicroservice.Application.PaymentTransactionCQ.Queries.GetAllPaymentTransactionByFilter;
|
||||
|
||||
public record GetAllPaymentTransactionByFilterQuery : IRequest<GetAllPaymentTransactionByFilterResponseDto>
|
||||
{
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
public string? SortBy { get; init; }
|
||||
public GetAllPaymentTransactionByFilterFilter? Filter { get; init; }
|
||||
}
|
||||
|
||||
public class GetAllPaymentTransactionByFilterFilter
|
||||
{
|
||||
public long? UserId { get; set; }
|
||||
public string? Mobile { get; set; }
|
||||
public string? GatewayProvider { get; set; }
|
||||
public bool? PaymentStatus { get; set; }
|
||||
public string? Authority { get; set; }
|
||||
public string? RefId { get; set; }
|
||||
public string? OrderId { get; set; }
|
||||
public DateTime? CreatedFrom { get; set; }
|
||||
public DateTime? CreatedTo { get; set; }
|
||||
}
|
||||
|
||||
public class GetAllPaymentTransactionByFilterResponseDto
|
||||
{
|
||||
public MetaData MetaData { get; set; }
|
||||
public List<GetAllPaymentTransactionByFilterResponseModel>? Models { get; set; }
|
||||
}
|
||||
|
||||
public class GetAllPaymentTransactionByFilterResponseModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long? UserId { get; set; }
|
||||
public string? Mobile { get; set; }
|
||||
public string GatewayProvider { get; set; } = string.Empty;
|
||||
public long Amount { get; set; }
|
||||
public string? Authority { get; set; }
|
||||
public bool PaymentStatus { get; set; }
|
||||
public int? VerificationStatusCode { get; set; }
|
||||
public string? VerificationStatusMessage { get; set; }
|
||||
public string? RefId { get; set; }
|
||||
public string? CardPan { get; set; }
|
||||
public string? OrderId { get; set; }
|
||||
public long? TransactionId { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
public string? UserFullName { get; set; }
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
namespace CMSMicroservice.Application.PaymentTransactionCQ.Queries.GetAllPaymentTransactionByFilter;
|
||||
|
||||
public class GetAllPaymentTransactionByFilterQueryHandler
|
||||
: IRequestHandler<GetAllPaymentTransactionByFilterQuery, GetAllPaymentTransactionByFilterResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetAllPaymentTransactionByFilterQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetAllPaymentTransactionByFilterResponseDto> Handle(
|
||||
GetAllPaymentTransactionByFilterQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.PaymentTransactions
|
||||
.AsNoTracking()
|
||||
.ApplyPaymentTransactionFilter(request.Filter)
|
||||
.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
||||
|
||||
var models = await (
|
||||
from x in query.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
join u in _context.Users.AsNoTracking() on x.UserId equals u.Id into ug
|
||||
from u in ug.DefaultIfEmpty()
|
||||
select new GetAllPaymentTransactionByFilterResponseModel
|
||||
{
|
||||
Id = x.Id,
|
||||
UserId = x.UserId,
|
||||
Mobile = x.Mobile,
|
||||
GatewayProvider = x.GatewayProvider,
|
||||
Amount = x.Amount,
|
||||
Authority = x.Authority,
|
||||
PaymentStatus = x.PaymentStatus,
|
||||
VerificationStatusCode = x.VerificationStatusCode,
|
||||
VerificationStatusMessage = x.VerificationStatusMessage,
|
||||
RefId = x.RefId,
|
||||
CardPan = x.CardPan,
|
||||
OrderId = x.OrderId,
|
||||
TransactionId = x.TransactionId,
|
||||
Description = x.Description,
|
||||
Created = x.Created,
|
||||
UserFullName = u != null
|
||||
? ((u.FirstName ?? "") + " " + (u.LastName ?? "")).Trim()
|
||||
: null
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var model in models)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.UserFullName))
|
||||
model.UserFullName = "—";
|
||||
}
|
||||
|
||||
return new GetAllPaymentTransactionByFilterResponseDto
|
||||
{
|
||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||
Models = models
|
||||
};
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
using CMSMicroservice.Domain.Entities.Payment;
|
||||
|
||||
namespace CMSMicroservice.Application.PaymentTransactionCQ.Queries.GetAllPaymentTransactionByFilter;
|
||||
|
||||
public static class PaymentTransactionFilterExtensions
|
||||
{
|
||||
public static IQueryable<PaymentTransaction> ApplyPaymentTransactionFilter(
|
||||
this IQueryable<PaymentTransaction> query,
|
||||
GetAllPaymentTransactionByFilterFilter? filter)
|
||||
{
|
||||
if (filter == null) return query;
|
||||
|
||||
if (filter.UserId.HasValue)
|
||||
query = query.Where(x => x.UserId == filter.UserId.Value);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Mobile))
|
||||
query = query.Where(x => x.Mobile != null && x.Mobile.Contains(filter.Mobile));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.GatewayProvider))
|
||||
query = query.Where(x => x.GatewayProvider == filter.GatewayProvider);
|
||||
|
||||
if (filter.PaymentStatus.HasValue)
|
||||
query = query.Where(x => x.PaymentStatus == filter.PaymentStatus.Value);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Authority))
|
||||
query = query.Where(x => x.Authority != null && x.Authority.Contains(filter.Authority));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.RefId))
|
||||
query = query.Where(x => x.RefId != null && x.RefId.Contains(filter.RefId));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.OrderId))
|
||||
query = query.Where(x => x.OrderId != null && x.OrderId.Contains(filter.OrderId));
|
||||
|
||||
if (filter.CreatedFrom.HasValue)
|
||||
query = query.Where(x => x.Created >= filter.CreatedFrom.Value);
|
||||
|
||||
if (filter.CreatedTo.HasValue)
|
||||
query = query.Where(x => x.Created <= filter.CreatedTo.Value);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
namespace CMSMicroservice.Application.PaymentTransactionCQ.Queries.GetPaymentTransactionSummary;
|
||||
|
||||
public record GetPaymentTransactionSummaryQuery : IRequest<GetPaymentTransactionSummaryResponseDto>
|
||||
{
|
||||
public GetAllPaymentTransactionByFilter.GetAllPaymentTransactionByFilterFilter? Filter { get; init; }
|
||||
}
|
||||
|
||||
public class GetPaymentTransactionSummaryResponseDto
|
||||
{
|
||||
public long TotalCount { get; set; }
|
||||
public long TotalAmount { get; set; }
|
||||
public long SuccessfulCount { get; set; }
|
||||
public long PendingCount { get; set; }
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
using CMSMicroservice.Application.PaymentTransactionCQ.Queries.GetAllPaymentTransactionByFilter;
|
||||
|
||||
namespace CMSMicroservice.Application.PaymentTransactionCQ.Queries.GetPaymentTransactionSummary;
|
||||
|
||||
public class GetPaymentTransactionSummaryQueryHandler
|
||||
: IRequestHandler<GetPaymentTransactionSummaryQuery, GetPaymentTransactionSummaryResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetPaymentTransactionSummaryQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetPaymentTransactionSummaryResponseDto> Handle(
|
||||
GetPaymentTransactionSummaryQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.PaymentTransactions
|
||||
.AsNoTracking()
|
||||
.ApplyPaymentTransactionFilter(request.Filter);
|
||||
|
||||
var totalCount = await query.LongCountAsync(cancellationToken);
|
||||
var totalAmount = totalCount > 0 ? await query.SumAsync(x => x.Amount, cancellationToken) : 0;
|
||||
var successfulCount = await query.LongCountAsync(x => x.PaymentStatus, cancellationToken);
|
||||
var pendingCount = await query.LongCountAsync(x => !x.PaymentStatus, cancellationToken);
|
||||
|
||||
return new GetPaymentTransactionSummaryResponseDto
|
||||
{
|
||||
TotalCount = totalCount,
|
||||
TotalAmount = totalAmount,
|
||||
SuccessfulCount = successfulCount,
|
||||
PendingCount = pendingCount
|
||||
};
|
||||
}
|
||||
}
|
||||
+4
@@ -21,6 +21,10 @@ public class GetCustomerProductsByFilterQuery : IRequest<GetCustomerProductsByFi
|
||||
public int? SaleCount { get; set; }
|
||||
public int? ViewCount { get; set; }
|
||||
public int? RemainingCount { get; set; }
|
||||
/// <summary>
|
||||
/// true = فقط موجود (RemainingCount > 0)، false = فقط ناموجود
|
||||
/// </summary>
|
||||
public bool? InStock { get; set; }
|
||||
public bool? IsActive { get; set; }
|
||||
public List<long>? CategoryIds { get; set; }
|
||||
}
|
||||
|
||||
+7
-5
@@ -56,17 +56,19 @@ public class GetCustomerProductsByFilterQueryHandler : IRequestHandler<GetCustom
|
||||
if (request.RemainingCount.HasValue)
|
||||
query = query.Where(x => x.RemainingCount == request.RemainingCount.Value);
|
||||
|
||||
if (request.InStock == true)
|
||||
query = query.Where(x => x.RemainingCount > 0);
|
||||
else if (request.InStock == false)
|
||||
query = query.Where(x => x.RemainingCount <= 0);
|
||||
|
||||
if (request.CategoryIds != null && request.CategoryIds.Any())
|
||||
query = query.Where(x => x.ProductCategories.Any(pc => request.CategoryIds.Contains(pc.CategoryId)));
|
||||
|
||||
if (request.IsActive.HasValue)
|
||||
query = query.Where(x => x.IsDeleted != request.IsActive.Value);
|
||||
|
||||
// Apply sorting
|
||||
if (!string.IsNullOrEmpty(request.SortBy))
|
||||
query = query.ApplyOrder(request.SortBy);
|
||||
else
|
||||
query = query.OrderByDescending(x => x.Created);
|
||||
// موجودیدارها اول، سپس سورت درخواستی
|
||||
query = query.ApplyOrderWithInStockPriority(request.SortBy, x => x.RemainingCount > 0);
|
||||
|
||||
// Pagination
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
+4
@@ -26,4 +26,8 @@ public record GetAllTransactionsByFilterQuery : IRequest<GetAllTransactionsByFil
|
||||
public string? RefId { get; set; }
|
||||
//
|
||||
public TransactionType? Type { get; set; }
|
||||
public DateTime? CreatedFrom { get; set; }
|
||||
public DateTime? CreatedTo { get; set; }
|
||||
public DateTime? PaymentDateFrom { get; set; }
|
||||
public DateTime? PaymentDateTo { get; set; }
|
||||
}
|
||||
|
||||
+104
-17
@@ -1,4 +1,7 @@
|
||||
using CMSMicroservice.Application.Common;
|
||||
|
||||
namespace CMSMicroservice.Application.TransactionsCQ.Queries.GetAllTransactionsByFilter;
|
||||
|
||||
public class GetAllTransactionsByFilterQueryHandler : IRequestHandler<GetAllTransactionsByFilterQuery, GetAllTransactionsByFilterResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
@@ -11,26 +14,110 @@ public class GetAllTransactionsByFilterQueryHandler : IRequestHandler<GetAllTran
|
||||
public async Task<GetAllTransactionsByFilterResponseDto> Handle(GetAllTransactionsByFilterQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.Transactions
|
||||
.ApplyOrder(sortBy: request.SortBy)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
if (request.Filter is not null)
|
||||
{
|
||||
query = query
|
||||
.Where(x => request.Filter.Id == null || x.Id == request.Filter.Id)
|
||||
.Where(x => request.Filter.Amount == null || x.Amount == request.Filter.Amount)
|
||||
.Where(x => request.Filter.Description == null || x.Description.Contains(request.Filter.Description))
|
||||
.Where(x => request.Filter.PaymentStatus == null || x.PaymentStatus== request.Filter.PaymentStatus)
|
||||
.Where(x => request.Filter.PaymentDate == null || x.PaymentDate == request.Filter.PaymentDate)
|
||||
.Where(x => request.Filter.RefId == null || x.RefId.Contains(request.Filter.RefId))
|
||||
.Where(x => request.Filter.Type == null || x.Type == request.Filter.Type)
|
||||
;
|
||||
}
|
||||
.ApplyTransactionFilter(request.Filter)
|
||||
.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
||||
|
||||
var models = await query
|
||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.Select(x => new GetAllTransactionsByFilterResponseModel
|
||||
{
|
||||
Id = x.Id,
|
||||
Amount = x.Amount,
|
||||
Description = x.Description,
|
||||
PaymentStatus = x.PaymentStatus,
|
||||
PaymentDate = x.PaymentDate,
|
||||
RefId = x.RefId,
|
||||
Type = x.Type,
|
||||
Created = x.Created
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
await PopulateUserInfoAsync(models, cancellationToken);
|
||||
|
||||
return new GetAllTransactionsByFilterResponseDto
|
||||
{
|
||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.ProjectToType<GetAllTransactionsByFilterResponseModel>().ToListAsync(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 = "—";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -24,4 +24,8 @@ public class GetAllTransactionsByFilterResponseDto
|
||||
public string? RefId { get; set; }
|
||||
//
|
||||
public TransactionType Type { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public long? UserId { get; set; }
|
||||
public string? UserFullName { get; set; }
|
||||
public string? Mobile { get; set; }
|
||||
}
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
|
||||
namespace CMSMicroservice.Application.TransactionsCQ.Queries.GetAllTransactionsByFilter;
|
||||
|
||||
public static class TransactionFilterExtensions
|
||||
{
|
||||
public static IQueryable<Transaction> ApplyTransactionFilter(
|
||||
this IQueryable<Transaction> query,
|
||||
GetAllTransactionsByFilterFilter? filter)
|
||||
{
|
||||
if (filter == null) return query;
|
||||
|
||||
if (filter.Id.HasValue)
|
||||
query = query.Where(x => x.Id == filter.Id.Value);
|
||||
|
||||
if (filter.Amount.HasValue)
|
||||
query = query.Where(x => x.Amount == filter.Amount.Value);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Description))
|
||||
query = query.Where(x => x.Description.Contains(filter.Description));
|
||||
|
||||
if (filter.PaymentStatus.HasValue)
|
||||
query = query.Where(x => x.PaymentStatus == filter.PaymentStatus.Value);
|
||||
|
||||
if (filter.PaymentDate.HasValue)
|
||||
query = query.Where(x => x.PaymentDate == filter.PaymentDate.Value);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.RefId))
|
||||
query = query.Where(x => x.RefId != null && x.RefId.Contains(filter.RefId));
|
||||
|
||||
if (filter.Type.HasValue)
|
||||
query = query.Where(x => x.Type == filter.Type.Value);
|
||||
|
||||
if (filter.CreatedFrom.HasValue)
|
||||
query = query.Where(x => x.Created >= filter.CreatedFrom.Value);
|
||||
|
||||
if (filter.CreatedTo.HasValue)
|
||||
query = query.Where(x => x.Created <= filter.CreatedTo.Value);
|
||||
|
||||
if (filter.PaymentDateFrom.HasValue)
|
||||
query = query.Where(x => x.PaymentDate >= filter.PaymentDateFrom.Value);
|
||||
|
||||
if (filter.PaymentDateTo.HasValue)
|
||||
query = query.Where(x => x.PaymentDate <= filter.PaymentDateTo.Value);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
using CMSMicroservice.Application.TransactionsCQ.Queries.GetAllTransactionsByFilter;
|
||||
|
||||
namespace CMSMicroservice.Application.TransactionsCQ.Queries.GetTransactionSummary;
|
||||
|
||||
public record GetTransactionSummaryQuery : IRequest<GetTransactionSummaryResponseDto>
|
||||
{
|
||||
public GetAllTransactionsByFilterFilter? Filter { get; init; }
|
||||
}
|
||||
|
||||
public class GetTransactionSummaryResponseDto
|
||||
{
|
||||
public long TotalCount { get; set; }
|
||||
public long TotalAmount { get; set; }
|
||||
public long SuccessCount { get; set; }
|
||||
public long PendingCount { get; set; }
|
||||
public long RejectCount { get; set; }
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
using CMSMicroservice.Application.TransactionsCQ.Queries.GetAllTransactionsByFilter;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.TransactionsCQ.Queries.GetTransactionSummary;
|
||||
|
||||
public class GetTransactionSummaryQueryHandler
|
||||
: IRequestHandler<GetTransactionSummaryQuery, GetTransactionSummaryResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetTransactionSummaryQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetTransactionSummaryResponseDto> Handle(
|
||||
GetTransactionSummaryQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.Transactions
|
||||
.AsNoTracking()
|
||||
.ApplyTransactionFilter(request.Filter);
|
||||
|
||||
var totalCount = await query.LongCountAsync(cancellationToken);
|
||||
var totalAmount = totalCount > 0 ? await query.SumAsync(x => x.Amount, cancellationToken) : 0;
|
||||
var successCount = await query.LongCountAsync(x => x.PaymentStatus == PaymentStatus.Success, cancellationToken);
|
||||
var pendingCount = await query.LongCountAsync(x => x.PaymentStatus == PaymentStatus.Pending, cancellationToken);
|
||||
var rejectCount = await query.LongCountAsync(x => x.PaymentStatus == PaymentStatus.Reject, cancellationToken);
|
||||
|
||||
return new GetTransactionSummaryResponseDto
|
||||
{
|
||||
TotalCount = totalCount,
|
||||
TotalAmount = totalAmount,
|
||||
SuccessCount = successCount,
|
||||
PendingCount = pendingCount,
|
||||
RejectCount = rejectCount
|
||||
};
|
||||
}
|
||||
}
|
||||
+2
@@ -9,5 +9,7 @@ public record VerifyOtpTokenCommand : IRequest<VerifyOtpTokenResponseDto>
|
||||
public string Code { get; init; }
|
||||
//کد معرف والد
|
||||
public string? ParentReferralCode { get; init; }
|
||||
//شماره موبایل دعوتکننده (اختیاری)
|
||||
public string? InviterMobile { get; init; }
|
||||
|
||||
}
|
||||
+37
@@ -118,6 +118,34 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
|
||||
else
|
||||
return new VerifyOtpTokenResponseDto { Success = false, Message = "ظرفیت معرف تکمیل است!!" };
|
||||
|
||||
// ── دعوتکننده (اختیاری؛ اعتبارسنجی سخت قبل از ایجاد کاربر) ──
|
||||
User? inviter = null;
|
||||
if (!string.IsNullOrWhiteSpace(request.InviterMobile))
|
||||
{
|
||||
var inviterMobile = request.InviterMobile.NormalizeIranMobile();
|
||||
if (inviterMobile == mobile)
|
||||
{
|
||||
return new VerifyOtpTokenResponseDto
|
||||
{
|
||||
Success = false,
|
||||
Message = "شماره موبایل دعوتکننده نمیتواند با شماره خودتان یکسان باشد."
|
||||
};
|
||||
}
|
||||
|
||||
inviter = await _context.Users
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(u => u.Mobile == inviterMobile, cancellationToken);
|
||||
|
||||
if (inviter == null)
|
||||
{
|
||||
return new VerifyOtpTokenResponseDto
|
||||
{
|
||||
Success = false,
|
||||
Message = "دعوتکننده با این شماره موبایل یافت نشد."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ایجاد کاربر جدید
|
||||
user = new User
|
||||
{
|
||||
@@ -134,6 +162,15 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
|
||||
user.AddDomainEvent(new CreateNewUserEvent(user));
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
if (inviter != null)
|
||||
{
|
||||
await _context.UserInvitations.AddAsync(new UserInvitation
|
||||
{
|
||||
InviterUserId = inviter.Id,
|
||||
InviteeUserId = user.Id
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
// ایجاد نقش کاربری
|
||||
var userRole = new UserRole { UserId = user.Id, RoleId = 1 };
|
||||
await _context.UserRoles.AddAsync(userRole, cancellationToken);
|
||||
|
||||
+30
-5
@@ -17,7 +17,7 @@ public class GetAllUserByFilterQueryHandler : IRequestHandler<GetAllUserByFilter
|
||||
if (request.Filter is not null)
|
||||
{
|
||||
query = query
|
||||
.Where(x => request.Filter.SearchText == null ||
|
||||
.Where(x => request.Filter.SearchText == null ||
|
||||
x.FirstName.Contains(request.Filter.SearchText) ||
|
||||
x.LastName.Contains(request.Filter.SearchText) ||
|
||||
x.Mobile.Contains(request.Filter.SearchText) ||
|
||||
@@ -34,14 +34,39 @@ public class GetAllUserByFilterQueryHandler : IRequestHandler<GetAllUserByFilter
|
||||
.Where(x => request.Filter.SmsNotifications == null || x.SmsNotifications == request.Filter.SmsNotifications)
|
||||
.Where(x => request.Filter.EmailNotifications == null || x.EmailNotifications == request.Filter.EmailNotifications)
|
||||
.Where(x => request.Filter.PushNotifications == null || x.PushNotifications == request.Filter.PushNotifications)
|
||||
.Where(x => request.Filter.BirthDate == null || x.BirthDate == request.Filter.BirthDate)
|
||||
;
|
||||
.Where(x => request.Filter.BirthDate == null || x.BirthDate == request.Filter.BirthDate);
|
||||
}
|
||||
|
||||
return new GetAllUserByFilterResponseDto
|
||||
{
|
||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.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 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.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
@@ -50,8 +51,10 @@ public class GetCustomerOrderQueryHandler : IRequestHandler<GetCustomerOrderQuer
|
||||
DeliveryStatus = order.DeliveryStatus,
|
||||
TrackingCode = order.TrackingCode ?? "",
|
||||
DeliveryDescription = order.DeliveryDescription ?? "",
|
||||
UserFullName = $"{order.User?.FirstName ?? ""} {order.User?.LastName ?? ""}".Trim(),
|
||||
UserFullName = UserDisplayName.From(order.User),
|
||||
UserNationalCode = order.User?.NationalCode ?? "",
|
||||
PostalCode = order.UserAddress?.PostalCode ?? "",
|
||||
UserMobile = order.User?.Mobile ?? "",
|
||||
VatAmount = order.OrderVAT?.VATAmount ?? 0,
|
||||
VatPercentage = order.OrderVAT != null ? (double)order.OrderVAT.VATRate * 100 : 0,
|
||||
FactorDetails = order.FactorDetails?.Select(fd => new FactorDetailDto
|
||||
|
||||
+2
@@ -20,6 +20,8 @@ public class GetCustomerOrderResponseDto
|
||||
public string DeliveryDescription { get; set; }
|
||||
public string UserFullName { get; set; }
|
||||
public string UserNationalCode { get; set; }
|
||||
public string PostalCode { get; set; } = string.Empty;
|
||||
public string UserMobile { get; set; } = string.Empty;
|
||||
public long VatAmount { get; set; }
|
||||
public double VatPercentage { get; set; }
|
||||
}
|
||||
|
||||
+2
@@ -113,6 +113,7 @@ public class GetCustomerOrderHistoryQueryHandler : IRequestHandler<GetCustomerOr
|
||||
DeliveryStatus.Delivered => 4,
|
||||
DeliveryStatus.Cancelled => 5,
|
||||
DeliveryStatus.Returned => 6,
|
||||
DeliveryStatus.ReadyForOfficePickup => 7,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
@@ -127,6 +128,7 @@ public class GetCustomerOrderHistoryQueryHandler : IRequestHandler<GetCustomerOr
|
||||
DeliveryStatus.Delivered => "تحویل داده شد",
|
||||
DeliveryStatus.Cancelled => "لغو شده",
|
||||
DeliveryStatus.Returned => "مرجوع شده",
|
||||
DeliveryStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
using CMSMicroservice.Application.Common;
|
||||
using CMSMicroservice.Application.Common.Extensions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
@@ -72,7 +73,7 @@ public class GetCustomerOrdersQueryHandler : IRequestHandler<GetCustomerOrdersQu
|
||||
DeliveryStatus = order.DeliveryStatus,
|
||||
TrackingCode = order.TrackingCode ?? "",
|
||||
DeliveryDescription = order.DeliveryDescription ?? "",
|
||||
UserFullName = $"{order.User?.FirstName ?? ""} {order.User?.LastName ?? ""}".Trim(),
|
||||
UserFullName = UserDisplayName.From(order.User),
|
||||
UserNationalCode = order.User?.NationalCode ?? "",
|
||||
VatAmount = order.OrderVAT?.VATAmount ?? 0,
|
||||
VatPercentage = order.OrderVAT != null ? (double)order.OrderVAT.VATRate * 100 : 0,
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetAllUserPackagePurchaseByFilter;
|
||||
|
||||
public record GetAllUserPackagePurchaseByFilterQuery : IRequest<GetAllUserPackagePurchaseByFilterResponseDto>
|
||||
{
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
public string? SortBy { get; init; }
|
||||
public GetAllUserPackagePurchaseByFilterFilter? Filter { get; init; }
|
||||
}
|
||||
|
||||
public class GetAllUserPackagePurchaseByFilterFilter
|
||||
{
|
||||
public long? UserId { get; set; }
|
||||
public long? PackageId { get; set; }
|
||||
public PackagePurchaseMethod? PurchaseMethod { get; set; }
|
||||
public DateTime? PurchasedFrom { get; set; }
|
||||
public DateTime? PurchasedTo { get; set; }
|
||||
public long? MinAmount { get; set; }
|
||||
public long? MaxAmount { get; set; }
|
||||
}
|
||||
|
||||
public class GetAllUserPackagePurchaseByFilterResponseDto
|
||||
{
|
||||
public MetaData MetaData { get; set; }
|
||||
public List<GetAllUserPackagePurchaseByFilterResponseModel>? Models { get; set; }
|
||||
}
|
||||
|
||||
public class GetAllUserPackagePurchaseByFilterResponseModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string UserMobile { get; set; } = string.Empty;
|
||||
public long PackageId { get; set; }
|
||||
public string PackageName { get; set; } = string.Empty;
|
||||
public PackagePurchaseMethod PurchaseMethod { get; set; }
|
||||
public DateTime PurchasedAt { get; set; }
|
||||
public long Amount { get; set; }
|
||||
public long? OrderId { get; set; }
|
||||
public long? TransactionId { get; set; }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
namespace CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetAllUserPackagePurchaseByFilter;
|
||||
|
||||
public class GetAllUserPackagePurchaseByFilterQueryHandler
|
||||
: IRequestHandler<GetAllUserPackagePurchaseByFilterQuery, GetAllUserPackagePurchaseByFilterResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetAllUserPackagePurchaseByFilterQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetAllUserPackagePurchaseByFilterResponseDto> Handle(
|
||||
GetAllUserPackagePurchaseByFilterQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.UserPackagePurchases
|
||||
.Include(x => x.User)
|
||||
.Include(x => x.Package)
|
||||
.AsNoTracking()
|
||||
.ApplyUserPackagePurchaseFilter(request.Filter)
|
||||
.ApplyOrder(sortBy: request.SortBy ?? "-PurchasedAt");
|
||||
|
||||
return new GetAllUserPackagePurchaseByFilterResponseDto
|
||||
{
|
||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||
Models = await query
|
||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.Select(x => new GetAllUserPackagePurchaseByFilterResponseModel
|
||||
{
|
||||
Id = x.Id,
|
||||
UserId = x.UserId,
|
||||
UserName = x.User != null ? $"{x.User.FirstName} {x.User.LastName}" : string.Empty,
|
||||
UserMobile = x.User != null ? x.User.Mobile : string.Empty,
|
||||
PackageId = x.PackageId,
|
||||
PackageName = x.Package != null ? x.Package.Title : string.Empty,
|
||||
PurchaseMethod = x.PurchaseMethod,
|
||||
PurchasedAt = x.PurchasedAt,
|
||||
Amount = x.Amount,
|
||||
OrderId = x.OrderId,
|
||||
TransactionId = x.TransactionId,
|
||||
Created = x.Created
|
||||
})
|
||||
.ToListAsync(cancellationToken)
|
||||
};
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
|
||||
namespace CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetAllUserPackagePurchaseByFilter;
|
||||
|
||||
public static class UserPackagePurchaseFilterExtensions
|
||||
{
|
||||
public static IQueryable<UserPackagePurchase> ApplyUserPackagePurchaseFilter(
|
||||
this IQueryable<UserPackagePurchase> query,
|
||||
GetAllUserPackagePurchaseByFilterFilter? filter)
|
||||
{
|
||||
if (filter == null) return query;
|
||||
|
||||
if (filter.UserId.HasValue)
|
||||
query = query.Where(x => x.UserId == filter.UserId.Value);
|
||||
|
||||
if (filter.PackageId.HasValue)
|
||||
query = query.Where(x => x.PackageId == filter.PackageId.Value);
|
||||
|
||||
if (filter.PurchaseMethod.HasValue)
|
||||
query = query.Where(x => x.PurchaseMethod == filter.PurchaseMethod.Value);
|
||||
|
||||
if (filter.PurchasedFrom.HasValue)
|
||||
query = query.Where(x => x.PurchasedAt >= filter.PurchasedFrom.Value);
|
||||
|
||||
if (filter.PurchasedTo.HasValue)
|
||||
query = query.Where(x => x.PurchasedAt <= filter.PurchasedTo.Value);
|
||||
|
||||
if (filter.MinAmount.HasValue)
|
||||
query = query.Where(x => x.Amount >= filter.MinAmount.Value);
|
||||
|
||||
if (filter.MaxAmount.HasValue)
|
||||
query = query.Where(x => x.Amount <= filter.MaxAmount.Value);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
+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
|
||||
};
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
namespace CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetUserPackagePurchaseSummary;
|
||||
|
||||
public record GetUserPackagePurchaseSummaryQuery : IRequest<GetUserPackagePurchaseSummaryResponseDto>
|
||||
{
|
||||
public GetAllUserPackagePurchaseByFilter.GetAllUserPackagePurchaseByFilterFilter? Filter { get; init; }
|
||||
}
|
||||
|
||||
public class GetUserPackagePurchaseSummaryResponseDto
|
||||
{
|
||||
public long TotalCount { get; set; }
|
||||
public long TotalAmount { get; set; }
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
using CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetAllUserPackagePurchaseByFilter;
|
||||
|
||||
namespace CMSMicroservice.Application.UserPackagePurchaseCQ.Queries.GetUserPackagePurchaseSummary;
|
||||
|
||||
public class GetUserPackagePurchaseSummaryQueryHandler
|
||||
: IRequestHandler<GetUserPackagePurchaseSummaryQuery, GetUserPackagePurchaseSummaryResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetUserPackagePurchaseSummaryQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetUserPackagePurchaseSummaryResponseDto> Handle(
|
||||
GetUserPackagePurchaseSummaryQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.UserPackagePurchases
|
||||
.AsNoTracking()
|
||||
.ApplyUserPackagePurchaseFilter(request.Filter);
|
||||
|
||||
var totalCount = await query.LongCountAsync(cancellationToken);
|
||||
var totalAmount = totalCount > 0 ? await query.SumAsync(x => x.Amount, cancellationToken) : 0;
|
||||
|
||||
return new GetUserPackagePurchaseSummaryResponseDto
|
||||
{
|
||||
TotalCount = totalCount,
|
||||
TotalAmount = totalAmount
|
||||
};
|
||||
}
|
||||
}
|
||||
+4
@@ -20,6 +20,10 @@ public class GetAllUserWalletHistoryByFilterResponseDto
|
||||
public long CurrentNetworkBalance { get; set; }
|
||||
//تغییر موجودی شبکه
|
||||
public long ChangeNerworkValue { get; set; }
|
||||
//موجودی جاری اعتباری
|
||||
public long CurrentDiscountBalance { get; set; }
|
||||
//تغییر موجودی اعتباری
|
||||
public long ChangeDiscountValue { get; set; }
|
||||
//افزایشی؟
|
||||
public bool IsIncrease { get; set; }
|
||||
//شناسه ارجاع
|
||||
|
||||
+4
@@ -13,6 +13,10 @@ public class GetUserWalletHistoryResponseDto
|
||||
public long CurrentNetworkBalance { get; set; }
|
||||
//تغییر موجودی شبکه
|
||||
public long ChangeNerworkValue { get; set; }
|
||||
//موجودی جاری اعتباری
|
||||
public long CurrentDiscountBalance { get; set; }
|
||||
//تغییر موجودی اعتباری
|
||||
public long ChangeDiscountValue { get; set; }
|
||||
//افزایشی؟
|
||||
public bool IsIncrease { get; set; }
|
||||
//شناسه ارجاع
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.WalletCQ.Commands.AdminManualCreditCharge;
|
||||
|
||||
public class AdminManualCreditChargeCommand : IRequest<AdminManualCreditChargeResult>
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public long Amount { get; set; }
|
||||
public string? Note { get; set; }
|
||||
public string? PerformedByAdminId { get; set; }
|
||||
}
|
||||
|
||||
public class AdminManualCreditChargeResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public long TransactionId { get; set; }
|
||||
public long NewBalance { get; set; }
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
using CMSMicroservice.Application.Common;
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.WalletCQ.Commands.AdminManualCreditCharge;
|
||||
|
||||
public class AdminManualCreditChargeCommandHandler
|
||||
: IRequestHandler<AdminManualCreditChargeCommand, AdminManualCreditChargeResult>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ILogger<AdminManualCreditChargeCommandHandler> _logger;
|
||||
private readonly IUserPaymentLock _paymentLock;
|
||||
|
||||
public AdminManualCreditChargeCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
ILogger<AdminManualCreditChargeCommandHandler> logger,
|
||||
IUserPaymentLock paymentLock)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
_paymentLock = paymentLock;
|
||||
}
|
||||
|
||||
public Task<AdminManualCreditChargeResult> Handle(
|
||||
AdminManualCreditChargeCommand request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_paymentLock.ExecuteAsync(
|
||||
PaymentLockScopes.Initiate(request.UserId),
|
||||
PaymentLockStrategy.FailFast,
|
||||
ct => HandleCore(request, ct),
|
||||
cancellationToken);
|
||||
|
||||
private async Task<AdminManualCreditChargeResult> HandleCore(
|
||||
AdminManualCreditChargeCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken)
|
||||
?? throw new NotFoundException(nameof(User), request.UserId);
|
||||
|
||||
var wallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == user.Id, cancellationToken)
|
||||
?? throw new NotFoundException($"کیف پول کاربر با شناسه {request.UserId} یافت نشد");
|
||||
|
||||
var oldBalance = wallet.Balance;
|
||||
wallet.Balance += request.Amount;
|
||||
|
||||
var refId = $"MANUAL-{Guid.NewGuid():N}";
|
||||
var notePart = string.IsNullOrWhiteSpace(request.Note)
|
||||
? string.Empty
|
||||
: $" — {request.Note.Trim()}";
|
||||
var adminPart = string.IsNullOrWhiteSpace(request.PerformedByAdminId)
|
||||
? string.Empty
|
||||
: $" (ادمین: {request.PerformedByAdminId})";
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Amount = request.Amount,
|
||||
Description = $"شارژ دستی کیف پول اصلی - کاربر {user.Id}{notePart}{adminPart}",
|
||||
PaymentStatus = PaymentStatus.Success,
|
||||
PaymentDate = DateTime.Now,
|
||||
RefId = refId,
|
||||
Type = TransactionType.ManualCreditWalletCharge
|
||||
};
|
||||
|
||||
_context.Transactions.Add(transaction);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_context.UserWalletHistories.Add(new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
ChangeValue = request.Amount,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
ChangeDiscountValue = 0,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
});
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Manual credit charge: UserId={UserId}, Amount={Amount}, OldBalance={Old}, NewBalance={New}, TxId={TxId}, Admin={Admin}",
|
||||
user.Id,
|
||||
request.Amount,
|
||||
oldBalance,
|
||||
wallet.Balance,
|
||||
transaction.Id,
|
||||
request.PerformedByAdminId);
|
||||
|
||||
return new AdminManualCreditChargeResult
|
||||
{
|
||||
Success = true,
|
||||
Message = "شارژ دستی کیف پول اصلی با موفقیت انجام شد",
|
||||
TransactionId = transaction.Id,
|
||||
NewBalance = wallet.Balance
|
||||
};
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
using CMSMicroservice.Domain.Common;
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.WalletCQ.Commands.AdminManualCreditCharge;
|
||||
|
||||
public class AdminManualCreditChargeCommandValidator : AbstractValidator<AdminManualCreditChargeCommand>
|
||||
{
|
||||
public AdminManualCreditChargeCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.UserId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
|
||||
|
||||
RuleFor(x => x.Amount)
|
||||
.GreaterThanOrEqualTo(SystemConstants.DiscountWalletMinCharge)
|
||||
.WithMessage("حداقل مبلغ شارژ ۱۰,۰۰۰ تومان است")
|
||||
.LessThanOrEqualTo(SystemConstants.WalletMaxSafeAmount)
|
||||
.WithMessage("مبلغ وارد شده بیش از حد مجاز است");
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeCreditWallet;
|
||||
|
||||
/// <summary>
|
||||
/// دستور شارژ کیف پول اصلی از طریق درگاه
|
||||
/// </summary>
|
||||
public class ChargeCreditWalletCommand : IRequest<PaymentInitiateResult>
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public long Amount { get; set; }
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
using CMSMicroservice.Application.Common;
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Entities.Payment;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeCreditWallet;
|
||||
|
||||
public class ChargeCreditWalletCommandHandler
|
||||
: IRequestHandler<ChargeCreditWalletCommand, PaymentInitiateResult>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IPaymentGatewayService _paymentGateway;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<ChargeCreditWalletCommandHandler> _logger;
|
||||
private readonly IUserPaymentLock _paymentLock;
|
||||
|
||||
public ChargeCreditWalletCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IPaymentGatewayService paymentGateway,
|
||||
IConfiguration configuration,
|
||||
ILogger<ChargeCreditWalletCommandHandler> logger,
|
||||
IUserPaymentLock paymentLock)
|
||||
{
|
||||
_context = context;
|
||||
_paymentGateway = paymentGateway;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
_paymentLock = paymentLock;
|
||||
}
|
||||
|
||||
public Task<PaymentInitiateResult> Handle(
|
||||
ChargeCreditWalletCommand request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_paymentLock.ExecuteAsync(
|
||||
PaymentLockScopes.Initiate(request.UserId),
|
||||
PaymentLockStrategy.FailFast,
|
||||
ct => HandleCore(request, ct),
|
||||
cancellationToken);
|
||||
|
||||
private async Task<PaymentInitiateResult> HandleCore(
|
||||
ChargeCreditWalletCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Charging credit wallet for UserId: {UserId}, Amount: {Amount}",
|
||||
request.UserId,
|
||||
request.Amount);
|
||||
|
||||
var user = await _context.Users
|
||||
.Include(u => u.ClubMembership)
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken)
|
||||
?? throw new NotFoundException(nameof(User), request.UserId);
|
||||
|
||||
if (user.ClubMembership?.IsActive != true)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.");
|
||||
}
|
||||
|
||||
var wallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == request.UserId, cancellationToken)
|
||||
?? throw new NotFoundException("کیف پول کاربر یافت نشد");
|
||||
|
||||
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
|
||||
var callbackUrl = $"{frontOfficeBaseUrl}/profile/payment-callback?type=credit-wallet";
|
||||
|
||||
var paymentRequest = new PaymentRequest
|
||||
{
|
||||
Amount = request.Amount,
|
||||
UserId = user.Id,
|
||||
Mobile = user.Mobile ?? "",
|
||||
CallbackUrl = callbackUrl,
|
||||
Description = $"شارژ کیف پول اصلی - کاربر {user.Id}"
|
||||
};
|
||||
|
||||
var paymentResult = await _paymentGateway.InitiatePaymentAsync(paymentRequest);
|
||||
|
||||
if (!paymentResult.IsSuccess)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Payment gateway failed for UserId {UserId}: {ErrorMessage}",
|
||||
user.Id,
|
||||
paymentResult.ErrorMessage);
|
||||
|
||||
throw new Exception($"خطا در ارتباط با درگاه پرداخت: {paymentResult.ErrorMessage}");
|
||||
}
|
||||
|
||||
var paymentTx = new PaymentTransaction
|
||||
{
|
||||
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
|
||||
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
|
||||
Amount = request.Amount,
|
||||
CallbackUrl = callbackUrl,
|
||||
Description = $"شارژ کیف پول اصلی - کاربر {user.Id}",
|
||||
Mobile = user.Mobile,
|
||||
UserId = user.Id,
|
||||
RequestStatusCode = 100,
|
||||
RequestStatusMessage = "Success",
|
||||
Authority = paymentResult.RefId,
|
||||
PaymentStatus = false
|
||||
};
|
||||
_context.PaymentTransactions.Add(paymentTx);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Credit wallet charge initiated. UserId: {UserId}, RefId: {RefId}, PaymentTxId: {PaymentTxId}",
|
||||
user.Id,
|
||||
paymentResult.RefId,
|
||||
paymentTx.Id);
|
||||
|
||||
return paymentResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error in ChargeCreditWalletCommand for UserId: {UserId}",
|
||||
request.UserId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
using CMSMicroservice.Domain.Common;
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.WalletCQ.Commands.ChargeCreditWallet;
|
||||
|
||||
public class ChargeCreditWalletCommandValidator : AbstractValidator<ChargeCreditWalletCommand>
|
||||
{
|
||||
public ChargeCreditWalletCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.UserId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
|
||||
|
||||
RuleFor(x => x.Amount)
|
||||
.GreaterThanOrEqualTo(SystemConstants.DiscountWalletMinCharge)
|
||||
.WithMessage("حداقل مبلغ شارژ ۱۰,۰۰۰ تومان است")
|
||||
.LessThanOrEqualTo(SystemConstants.WalletMaxSafeAmount)
|
||||
.WithMessage("مبلغ وارد شده بیش از حد مجاز است");
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.WalletCQ.Commands.VerifyCreditWalletCharge;
|
||||
|
||||
/// <summary>
|
||||
/// دستور تأیید شارژ کیف پول اصلی
|
||||
/// </summary>
|
||||
public class VerifyCreditWalletChargeCommand : IRequest<bool>
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public long Amount { get; set; }
|
||||
public string Authority { get; set; } = string.Empty;
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
using CMSMicroservice.Application.Common;
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.WalletCQ.Commands.VerifyCreditWalletCharge;
|
||||
|
||||
public class VerifyCreditWalletChargeCommandHandler
|
||||
: IRequestHandler<VerifyCreditWalletChargeCommand, bool>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IPaymentGatewayService _paymentGateway;
|
||||
private readonly ILogger<VerifyCreditWalletChargeCommandHandler> _logger;
|
||||
private readonly IUserPaymentLock _paymentLock;
|
||||
|
||||
public VerifyCreditWalletChargeCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IPaymentGatewayService paymentGateway,
|
||||
ILogger<VerifyCreditWalletChargeCommandHandler> logger,
|
||||
IUserPaymentLock paymentLock)
|
||||
{
|
||||
_context = context;
|
||||
_paymentGateway = paymentGateway;
|
||||
_logger = logger;
|
||||
_paymentLock = paymentLock;
|
||||
}
|
||||
|
||||
public Task<bool> Handle(
|
||||
VerifyCreditWalletChargeCommand request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_paymentLock.ExecuteAsync(
|
||||
PaymentLockScopes.Verify(request.UserId, request.Authority),
|
||||
PaymentLockStrategy.WaitForRelease,
|
||||
ct => HandleCore(request, ct),
|
||||
cancellationToken);
|
||||
|
||||
private async Task<bool> HandleCore(
|
||||
VerifyCreditWalletChargeCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Verifying credit wallet charge. UserId: {UserId}, Amount: {Amount}, Authority: {Authority}",
|
||||
request.UserId,
|
||||
request.Amount,
|
||||
request.Authority);
|
||||
|
||||
var user = await _context.Users
|
||||
.Include(u => u.ClubMembership)
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken)
|
||||
?? throw new NotFoundException(nameof(User), request.UserId);
|
||||
|
||||
if (user.ClubMembership?.IsActive != true)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.");
|
||||
}
|
||||
|
||||
var paymentTx = await _context.PaymentTransactions
|
||||
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, cancellationToken);
|
||||
|
||||
if (paymentTx?.PaymentStatus == true)
|
||||
{
|
||||
_logger.LogWarning("PaymentTransaction already verified: {Authority}", request.Authority);
|
||||
return true;
|
||||
}
|
||||
|
||||
var amountInToman = (decimal)(paymentTx?.Amount ?? 0);
|
||||
|
||||
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
|
||||
request.Authority,
|
||||
"OK",
|
||||
amountInToman,
|
||||
cancellationToken);
|
||||
|
||||
if (paymentTx != null)
|
||||
{
|
||||
paymentTx.PaymentStatus = verifyResult.IsSuccess;
|
||||
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
|
||||
paymentTx.VerificationStatusMessage = verifyResult.Message;
|
||||
paymentTx.CardPan = verifyResult.CardPan;
|
||||
paymentTx.CardHash = verifyResult.CardHash;
|
||||
paymentTx.RefId = verifyResult.TrackingCode;
|
||||
}
|
||||
|
||||
if (!verifyResult.IsSuccess)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Credit wallet charge verification failed for UserId {UserId}: {Message}",
|
||||
request.UserId,
|
||||
verifyResult.Message);
|
||||
|
||||
throw new Exception($"تراکنش ناموفق: {verifyResult.Message}");
|
||||
}
|
||||
|
||||
var wallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == user.Id, cancellationToken)
|
||||
?? throw new NotFoundException($"کیف پول کاربر با شناسه {request.UserId} یافت نشد");
|
||||
|
||||
var oldBalance = wallet.Balance;
|
||||
wallet.Balance += request.Amount;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Charging credit balance for UserId {UserId}: {OldBalance} -> {NewBalance}",
|
||||
request.UserId,
|
||||
oldBalance,
|
||||
wallet.Balance);
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Amount = request.Amount,
|
||||
Description = $"شارژ کیف پول اصلی - کاربر {user.Id}",
|
||||
PaymentStatus = PaymentStatus.Success,
|
||||
PaymentDate = DateTime.Now,
|
||||
RefId = verifyResult.RefId,
|
||||
Type = TransactionType.CreditWalletCharge
|
||||
};
|
||||
|
||||
_context.Transactions.Add(transaction);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_context.UserWalletHistories.Add(new Domain.Entities.UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
ChangeValue = request.Amount,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
ChangeDiscountValue = 0,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
});
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
if (paymentTx != null)
|
||||
{
|
||||
paymentTx.TransactionId = transaction.Id;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Credit wallet charged successfully. UserId: {UserId}, TransactionId: {TransactionId}, RefId: {RefId}",
|
||||
user.Id,
|
||||
transaction.Id,
|
||||
verifyResult.RefId);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error in VerifyCreditWalletChargeCommand for UserId: {UserId}",
|
||||
request.UserId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.WalletCQ.Queries.GetMagicWalletCharges;
|
||||
|
||||
public class GetMagicWalletChargesQuery : IRequest<GetMagicWalletChargesResponseDto>
|
||||
{
|
||||
public PaginationState? PaginationState { get; set; }
|
||||
public string? SortBy { get; set; }
|
||||
public GetMagicWalletChargesFilter? Filter { get; set; }
|
||||
}
|
||||
|
||||
public class GetMagicWalletChargesFilter
|
||||
{
|
||||
public long? UserId { get; set; }
|
||||
public long? TransactionId { get; set; }
|
||||
public string? RefId { get; set; }
|
||||
public DateTime? CreatedFrom { get; set; }
|
||||
public DateTime? CreatedTo { get; set; }
|
||||
}
|
||||
|
||||
public class GetMagicWalletChargesResponseDto
|
||||
{
|
||||
public MetaData MetaData { get; set; } = new();
|
||||
public List<MagicWalletChargeModelDto> Models { get; set; } = new();
|
||||
}
|
||||
|
||||
public class MagicWalletChargeModelDto
|
||||
{
|
||||
public long TransactionId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public long DepositAmount { get; set; }
|
||||
public double Multiplier { get; set; }
|
||||
public long CreditedAmount { get; set; }
|
||||
public long? RemainingCapacity { get; set; }
|
||||
public string? RefId { get; set; }
|
||||
public long NewBalance { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime? PaymentDate { get; set; }
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
using CMSMicroservice.Application.Common.Extensions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.WalletCQ.Queries.GetMagicWalletCharges;
|
||||
|
||||
public class GetMagicWalletChargesQueryHandler
|
||||
: IRequestHandler<GetMagicWalletChargesQuery, GetMagicWalletChargesResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetMagicWalletChargesQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetMagicWalletChargesResponseDto> Handle(
|
||||
GetMagicWalletChargesQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = request.Filter;
|
||||
|
||||
var query =
|
||||
from t in _context.Transactions.AsNoTracking()
|
||||
where t.Type == TransactionType.MagicWalletDeposit
|
||||
join h in _context.UserWalletHistories.AsNoTracking() on t.Id equals h.RefrenceId
|
||||
join w in _context.UserWallets.AsNoTracking() on h.WalletId equals w.Id
|
||||
join u in _context.Users.AsNoTracking() on w.UserId equals u.Id
|
||||
select new MagicWalletChargeModelDto
|
||||
{
|
||||
TransactionId = t.Id,
|
||||
UserId = w.UserId,
|
||||
UserName = ((u.FirstName ?? "") + " " + (u.LastName ?? "")).Trim(),
|
||||
DepositAmount = t.Amount,
|
||||
CreditedAmount = h.ChangeValue,
|
||||
RefId = t.RefId,
|
||||
NewBalance = h.CurrentBalance,
|
||||
Created = t.Created,
|
||||
PaymentDate = t.PaymentDate
|
||||
};
|
||||
|
||||
if (filter?.UserId is long userId)
|
||||
query = query.Where(x => x.UserId == userId);
|
||||
|
||||
if (filter?.TransactionId is long txId)
|
||||
query = query.Where(x => x.TransactionId == txId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter?.RefId))
|
||||
query = query.Where(x => x.RefId != null && x.RefId.Contains(filter.RefId));
|
||||
|
||||
if (filter?.CreatedFrom is DateTime from)
|
||||
query = query.Where(x => x.Created >= from);
|
||||
|
||||
if (filter?.CreatedTo is DateTime to)
|
||||
query = query.Where(x => x.Created <= to);
|
||||
|
||||
query = query.OrderByDescending(x => x.Created);
|
||||
|
||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||
var models = await query
|
||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var model in models)
|
||||
{
|
||||
model.Multiplier = model.DepositAmount == 0
|
||||
? 0
|
||||
: (double)model.CreditedAmount / model.DepositAmount;
|
||||
}
|
||||
|
||||
await PopulateRemainingCapacityAsync(models, cancellationToken);
|
||||
|
||||
return new GetMagicWalletChargesResponseDto
|
||||
{
|
||||
MetaData = meta,
|
||||
Models = models
|
||||
};
|
||||
}
|
||||
|
||||
private async Task PopulateRemainingCapacityAsync(
|
||||
List<MagicWalletChargeModelDto> models,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (models.Count == 0)
|
||||
return;
|
||||
|
||||
var userIds = models.Select(m => m.UserId).Distinct().ToList();
|
||||
|
||||
var wallets = await _context.UserWallets.AsNoTracking()
|
||||
.Where(w => userIds.Contains(w.UserId))
|
||||
.Select(w => new { w.UserId, w.MagicActivatedAt })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var walletByUser = wallets.ToDictionary(w => w.UserId, w => w.MagicActivatedAt);
|
||||
|
||||
var cycles = await _context.ClubMembershipCycles.AsNoTracking()
|
||||
.Where(c => userIds.Contains(c.UserId) && c.IsCurrentCycle)
|
||||
.Select(c => new { c.UserId, c.PackageId })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var packageIds = cycles.Select(c => c.PackageId).Distinct().ToList();
|
||||
var packages = await _context.Packages.AsNoTracking()
|
||||
.Where(p => packageIds.Contains(p.Id) || (p.IsBasePackage && !p.IsDeleted))
|
||||
.Select(p => new { p.Id, p.IsBasePackage, p.IsDeleted, p.MagicWalletMaxDeposit })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var basePackageMax = packages
|
||||
.Where(p => p.IsBasePackage && !p.IsDeleted)
|
||||
.Select(p => (long?)p.MagicWalletMaxDeposit)
|
||||
.FirstOrDefault();
|
||||
|
||||
var packageIdByUser = cycles.ToDictionary(c => c.UserId, c => c.PackageId);
|
||||
var maxByPackageId = packages.ToDictionary(p => p.Id, p => p.MagicWalletMaxDeposit);
|
||||
|
||||
// All magic deposits for these users (for cumulative sum within magic cycle)
|
||||
var depositRows = await (
|
||||
from t in _context.Transactions.AsNoTracking()
|
||||
where t.Type == TransactionType.MagicWalletDeposit
|
||||
join h in _context.UserWalletHistories.AsNoTracking() on t.Id equals h.RefrenceId
|
||||
join w in _context.UserWallets.AsNoTracking() on h.WalletId equals w.Id
|
||||
where userIds.Contains(w.UserId)
|
||||
select new { w.UserId, t.Id, t.Amount, t.Created }
|
||||
).ToListAsync(cancellationToken);
|
||||
|
||||
var depositsByUser = depositRows
|
||||
.GroupBy(d => d.UserId)
|
||||
.ToDictionary(g => g.Key, g => g.OrderBy(x => x.Created).ThenBy(x => x.Id).ToList());
|
||||
|
||||
foreach (var model in models)
|
||||
{
|
||||
if (!walletByUser.TryGetValue(model.UserId, out var magicActivatedAt) || !magicActivatedAt.HasValue)
|
||||
{
|
||||
model.RemainingCapacity = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
long? maxDeposit = null;
|
||||
if (packageIdByUser.TryGetValue(model.UserId, out var pkgId) &&
|
||||
maxByPackageId.TryGetValue(pkgId, out var pkgMax))
|
||||
{
|
||||
maxDeposit = pkgMax;
|
||||
}
|
||||
else
|
||||
{
|
||||
maxDeposit = basePackageMax;
|
||||
}
|
||||
|
||||
if (maxDeposit is null)
|
||||
{
|
||||
model.RemainingCapacity = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!depositsByUser.TryGetValue(model.UserId, out var userDeposits))
|
||||
{
|
||||
model.RemainingCapacity = maxDeposit;
|
||||
continue;
|
||||
}
|
||||
|
||||
var depositedThroughThis = userDeposits
|
||||
.Where(d => d.Created >= magicActivatedAt.Value &&
|
||||
(d.Created < model.Created ||
|
||||
(d.Created == model.Created && d.Id <= model.TransactionId)))
|
||||
.Sum(d => d.Amount);
|
||||
|
||||
model.RemainingCapacity = Math.Max(0, maxDeposit.Value - depositedThroughThis);
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.WalletCQ.Queries.GetManualCreditCharges;
|
||||
|
||||
public class GetManualCreditChargesQuery : IRequest<GetManualCreditChargesResponseDto>
|
||||
{
|
||||
public PaginationState? PaginationState { get; set; }
|
||||
public string? SortBy { get; set; }
|
||||
public GetManualCreditChargesFilter? Filter { get; set; }
|
||||
}
|
||||
|
||||
public class GetManualCreditChargesFilter
|
||||
{
|
||||
public long? UserId { get; set; }
|
||||
public long? TransactionId { get; set; }
|
||||
public string? RefId { get; set; }
|
||||
public DateTime? CreatedFrom { get; set; }
|
||||
public DateTime? CreatedTo { get; set; }
|
||||
}
|
||||
|
||||
public class GetManualCreditChargesResponseDto
|
||||
{
|
||||
public MetaData MetaData { get; set; } = new();
|
||||
public List<ManualCreditChargeModelDto> Models { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ManualCreditChargeModelDto
|
||||
{
|
||||
public long TransactionId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public long Amount { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string? RefId { get; set; }
|
||||
public long NewBalance { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime? PaymentDate { get; set; }
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
using CMSMicroservice.Application.Common.Extensions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.WalletCQ.Queries.GetManualCreditCharges;
|
||||
|
||||
public class GetManualCreditChargesQueryHandler
|
||||
: IRequestHandler<GetManualCreditChargesQuery, GetManualCreditChargesResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetManualCreditChargesQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetManualCreditChargesResponseDto> Handle(
|
||||
GetManualCreditChargesQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = request.Filter;
|
||||
|
||||
var query =
|
||||
from t in _context.Transactions.AsNoTracking()
|
||||
where t.Type == TransactionType.ManualCreditWalletCharge
|
||||
join h in _context.UserWalletHistories.AsNoTracking() on t.Id equals h.RefrenceId
|
||||
join w in _context.UserWallets.AsNoTracking() on h.WalletId equals w.Id
|
||||
join u in _context.Users.AsNoTracking() on w.UserId equals u.Id
|
||||
select new ManualCreditChargeModelDto
|
||||
{
|
||||
TransactionId = t.Id,
|
||||
UserId = w.UserId,
|
||||
UserName = ((u.FirstName ?? "") + " " + (u.LastName ?? "")).Trim(),
|
||||
Amount = t.Amount,
|
||||
Description = t.Description ?? string.Empty,
|
||||
RefId = t.RefId,
|
||||
NewBalance = h.CurrentBalance,
|
||||
Created = t.Created,
|
||||
PaymentDate = t.PaymentDate
|
||||
};
|
||||
|
||||
if (filter?.UserId is long userId)
|
||||
query = query.Where(x => x.UserId == userId);
|
||||
|
||||
if (filter?.TransactionId is long txId)
|
||||
query = query.Where(x => x.TransactionId == txId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter?.RefId))
|
||||
query = query.Where(x => x.RefId != null && x.RefId.Contains(filter.RefId));
|
||||
|
||||
if (filter?.CreatedFrom is DateTime from)
|
||||
query = query.Where(x => x.Created >= from);
|
||||
|
||||
if (filter?.CreatedTo is DateTime to)
|
||||
query = query.Where(x => x.Created <= to);
|
||||
|
||||
query = query.OrderByDescending(x => x.Created);
|
||||
|
||||
return new GetManualCreditChargesResponseDto
|
||||
{
|
||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||
Models = await query
|
||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.ToListAsync(cancellationToken)
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user