Compare commits
50 Commits
5c1e3cf601
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
| ea7ba3bd3f | |||
| fe7179f6e9 | |||
| f79affb44d | |||
| 749cfeb8f4 | |||
| 6f573c0a29 | |||
| 9b82b45e46 | |||
| 39bbe59bbc | |||
| 5c852e499d | |||
| cbc1225562 | |||
| 6ad1341754 | |||
| e970e516ce | |||
| 9d17f174af | |||
| 29426022aa | |||
| 2b958bd214 | |||
| f31fb5ee39 | |||
| 0b7212ac1c | |||
| 59ed3857d4 | |||
| 78d1fee89a | |||
| 612d6fd141 | |||
| 87d61abc5b | |||
| ea1d2d4400 | |||
| c814cd9118 | |||
| 2d23dbc798 | |||
| 8ec0910cdf | |||
| e882f09955 | |||
| 436e5d7aca | |||
| 6a04a6a270 | |||
| 427d2cce1d | |||
| fb9e3d0109 | |||
| 6c90e4caf0 | |||
| 40da1bd8df | |||
| 053c5a7c4f | |||
| 3810146651 | |||
| 82944e11d0 | |||
| ce98efdd68 | |||
| d232399f43 | |||
| bb20ad2c4e | |||
| 246d893b62 | |||
| 99aab3beff | |||
| 728f28f525 | |||
| 89af07c122 | |||
| 58aeddf004 | |||
| 6c3fbe43b1 | |||
| 7570c39e65 | |||
| 46ac1d817e | |||
| db20d95b43 | |||
| f8794bbb63 | |||
| 19be5dc9dc | |||
| 6395f63ab5 | |||
| 758640cce3 |
@@ -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,11 +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 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 rollout restart deployment/cms &&
|
||||
kubectl rollout status deployment/cms --timeout=180s &&
|
||||
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": {
|
||||
|
||||
@@ -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,340 @@
|
||||
/*
|
||||
Cleanup all side-effects of a test FrontOffice user (register + package + club + wallet + invitations).
|
||||
|
||||
Usage:
|
||||
EXEC dbo.usp_CleanupTestUser @Mobile = N'09037437157', @DryRun = 1;
|
||||
EXEC dbo.usp_CleanupTestUser @Mobile = N'09037437157';
|
||||
EXEC dbo.usp_CleanupTestUser @Mobile = N'09...', @ReparentChildren = 1;
|
||||
EXEC dbo.usp_CleanupTestUser @Mobile = N'09...', @ReversePool = 1, @AbortIfPoolCalculated = 1;
|
||||
|
||||
Pool reverse: for each ClubMembershipCycle, subtract Packages.ActivationFee
|
||||
(fallback: ClubMembership.InitialContribution) from CMS.WeeklyCommissionPools
|
||||
for matching WeekDefinition × PackageId.
|
||||
|
||||
Does NOT delete pool rows (shared). Does NOT claw back commission already paid to OTHER users.
|
||||
Does NOT touch: Packages master, Contracts master, WeekDefinitions, Roles, catalogs.
|
||||
*/
|
||||
CREATE OR ALTER PROCEDURE dbo.usp_CleanupTestUser
|
||||
@UserId BIGINT = NULL,
|
||||
@Mobile NVARCHAR(20) = NULL,
|
||||
@HardDeleteUser BIT = 1,
|
||||
@ReparentChildren BIT = 0,
|
||||
@ReversePool BIT = 1,
|
||||
@AbortIfPoolCalculated BIT = 0,
|
||||
@DryRun BIT = 0
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
SET ANSI_NULLS ON;
|
||||
|
||||
IF @UserId IS NULL AND (@Mobile IS NULL OR LTRIM(RTRIM(@Mobile)) = N'')
|
||||
BEGIN
|
||||
RAISERROR(N'Provide @UserId and/or @Mobile.', 16, 1);
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
DECLARE @MobileNorm NVARCHAR(20) = NULL;
|
||||
IF @Mobile IS NOT NULL
|
||||
BEGIN
|
||||
SET @MobileNorm = LTRIM(RTRIM(@Mobile));
|
||||
IF LEFT(@MobileNorm, 3) = N'+98' SET @MobileNorm = N'0' + SUBSTRING(@MobileNorm, 4, 20);
|
||||
IF LEFT(@MobileNorm, 2) = N'98' AND LEN(@MobileNorm) = 12 SET @MobileNorm = N'0' + SUBSTRING(@MobileNorm, 3, 20);
|
||||
IF LEFT(@MobileNorm, 1) = N'9' AND LEN(@MobileNorm) = 10 SET @MobileNorm = N'0' + @MobileNorm;
|
||||
END;
|
||||
|
||||
IF @UserId IS NULL
|
||||
BEGIN
|
||||
SELECT TOP (1) @UserId = Id
|
||||
FROM CMS.Users
|
||||
WHERE Mobile = @MobileNorm
|
||||
OR Mobile = @Mobile
|
||||
OR REPLACE(Mobile, N'+98', N'0') = @MobileNorm
|
||||
ORDER BY IsDeleted ASC, Id DESC;
|
||||
END;
|
||||
|
||||
IF @UserId IS NULL
|
||||
BEGIN
|
||||
RAISERROR(N'User not found for given @UserId/@Mobile.', 16, 1);
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
DECLARE @UserMobile NVARCHAR(20);
|
||||
SELECT @UserMobile = Mobile FROM CMS.Users WHERE Id = @UserId;
|
||||
IF @UserMobile IS NULL
|
||||
BEGIN
|
||||
RAISERROR(N'UserId %I64d not found.', 16, 1, @UserId);
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
DECLARE @ChildCount INT =
|
||||
(SELECT COUNT(*) FROM CMS.Users WHERE NetworkParentId = @UserId AND IsDeleted = 0);
|
||||
|
||||
IF @ChildCount > 0 AND @ReparentChildren = 0 AND @HardDeleteUser = 1
|
||||
BEGIN
|
||||
RAISERROR(N'User has %d active network children. Pass @ReparentChildren=1 or soft-delete only.', 16, 1, @ChildCount);
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
PRINT N'=== usp_CleanupTestUser ===';
|
||||
PRINT N'UserId=' + CAST(@UserId AS NVARCHAR(20)) + N' Mobile=' + ISNULL(@UserMobile, N'?')
|
||||
+ N' DryRun=' + CAST(@DryRun AS NVARCHAR(1))
|
||||
+ N' ReversePool=' + CAST(@ReversePool AS NVARCHAR(1));
|
||||
|
||||
DECLARE @Wallets TABLE (WalletId BIGINT PRIMARY KEY);
|
||||
INSERT INTO @Wallets (WalletId)
|
||||
SELECT Id FROM CMS.UserWallets WHERE UserId = @UserId;
|
||||
|
||||
DECLARE @Tx TABLE (TransactionId BIGINT PRIMARY KEY);
|
||||
INSERT INTO @Tx (TransactionId)
|
||||
SELECT DISTINCT x.TransactionId
|
||||
FROM (
|
||||
SELECT TransactionId FROM CMS.UserPackagePurchases WHERE UserId = @UserId AND TransactionId IS NOT NULL
|
||||
UNION
|
||||
SELECT TransactionId FROM CMS.PaymentTransactions WHERE UserId = @UserId AND TransactionId IS NOT NULL
|
||||
UNION
|
||||
SELECT TransactionId FROM CMS.UserOrders WHERE UserId = @UserId AND TransactionId IS NOT NULL
|
||||
UNION
|
||||
SELECT TransactionId FROM CMS.ManualPayments WHERE UserId = @UserId AND TransactionId IS NOT NULL
|
||||
UNION
|
||||
SELECT TransactionId FROM CMS.DayaLoanContracts WHERE UserId = @UserId AND TransactionId IS NOT NULL
|
||||
UNION
|
||||
SELECT TransactionId FROM CMS.DiscountOrders WHERE UserId = @UserId AND TransactionId IS NOT NULL
|
||||
UNION
|
||||
SELECT h.RefrenceId
|
||||
FROM CMS.UserWalletHistories h
|
||||
INNER JOIN @Wallets w ON w.WalletId = h.WalletId
|
||||
WHERE h.RefrenceId IS NOT NULL
|
||||
) x
|
||||
WHERE x.TransactionId IS NOT NULL;
|
||||
|
||||
DECLARE @Orders TABLE (OrderId BIGINT PRIMARY KEY);
|
||||
INSERT INTO @Orders (OrderId)
|
||||
SELECT Id FROM CMS.UserOrders WHERE UserId = @UserId;
|
||||
|
||||
DECLARE @DiscountOrders TABLE (DiscountOrderId BIGINT PRIMARY KEY);
|
||||
INSERT INTO @DiscountOrders (DiscountOrderId)
|
||||
SELECT Id FROM CMS.DiscountOrders WHERE UserId = @UserId;
|
||||
|
||||
DECLARE @ClubMemberships TABLE (ClubMembershipId BIGINT PRIMARY KEY);
|
||||
INSERT INTO @ClubMemberships (ClubMembershipId)
|
||||
SELECT Id FROM CMS.ClubMemberships WHERE UserId = @UserId;
|
||||
|
||||
DECLARE @Cycles TABLE (CycleId BIGINT PRIMARY KEY);
|
||||
INSERT INTO @Cycles (CycleId)
|
||||
SELECT Id FROM CMS.ClubMembershipCycles WHERE UserId = @UserId
|
||||
UNION
|
||||
SELECT Id FROM CMS.ClubMembershipCycles WHERE ClubMembershipId IN (SELECT ClubMembershipId FROM @ClubMemberships);
|
||||
|
||||
DECLARE @Payouts TABLE (PayoutId BIGINT PRIMARY KEY);
|
||||
INSERT INTO @Payouts (PayoutId)
|
||||
SELECT Id FROM CMS.UserCommissionPayouts WHERE UserId = @UserId;
|
||||
|
||||
DECLARE @PoolReversals TABLE (
|
||||
CycleId BIGINT,
|
||||
PackageId BIGINT,
|
||||
WeekDefinitionId BIGINT NULL,
|
||||
PoolId BIGINT NULL,
|
||||
Fee BIGINT,
|
||||
PoolAmountBefore BIGINT NULL,
|
||||
IsCalculated BIT NULL,
|
||||
Note NVARCHAR(200)
|
||||
);
|
||||
|
||||
INSERT INTO @PoolReversals (CycleId, PackageId, WeekDefinitionId, PoolId, Fee, PoolAmountBefore, IsCalculated, Note)
|
||||
SELECT
|
||||
c.Id,
|
||||
c.PackageId,
|
||||
wd.Id,
|
||||
wp.Id,
|
||||
ISNULL(NULLIF(cm.InitialContribution, 0), ISNULL(p.ActivationFee, 0)),
|
||||
wp.TotalPoolAmount,
|
||||
wp.IsCalculated,
|
||||
CASE
|
||||
WHEN wd.Id IS NULL THEN N'NO_WEEK_MATCH'
|
||||
WHEN wp.Id IS NULL THEN N'NO_POOL_ROW'
|
||||
WHEN wp.IsCalculated = 1 THEN N'POOL_ALREADY_CALCULATED'
|
||||
ELSE N'OK'
|
||||
END
|
||||
FROM CMS.ClubMembershipCycles c
|
||||
LEFT JOIN CMS.ClubMemberships cm ON cm.Id = c.ClubMembershipId
|
||||
LEFT JOIN CMS.Packages p ON p.Id = c.PackageId
|
||||
LEFT JOIN CMS.WeekDefinitions wd
|
||||
ON CAST(c.PackagePurchasedAt AS DATE) >= CAST(wd.StartDate AS DATE)
|
||||
AND CAST(c.PackagePurchasedAt AS DATE) <= CAST(wd.EndDate AS DATE)
|
||||
AND wd.IsDeleted = 0
|
||||
LEFT JOIN CMS.WeeklyCommissionPools wp
|
||||
ON wp.WeekDefinitionId = wd.Id
|
||||
AND wp.PackageId = c.PackageId
|
||||
AND wp.IsDeleted = 0
|
||||
WHERE c.Id IN (SELECT CycleId FROM @Cycles);
|
||||
|
||||
SELECT N'Users' AS Entity, COUNT(*) AS Cnt FROM CMS.Users WHERE Id = @UserId
|
||||
UNION ALL SELECT N'OtpTokens', COUNT(*) FROM CMS.OtpTokens WHERE Mobile IN (@UserMobile, @MobileNorm, @Mobile)
|
||||
UNION ALL SELECT N'UserInvitations', COUNT(*) FROM CMS.UserInvitations WHERE InviterUserId = @UserId OR InviteeUserId = @UserId
|
||||
UNION ALL SELECT N'ClubMemberships', COUNT(*) FROM @ClubMemberships
|
||||
UNION ALL SELECT N'ClubMembershipCycles', COUNT(*) FROM @Cycles
|
||||
UNION ALL SELECT N'UserPackagePurchases', COUNT(*) FROM CMS.UserPackagePurchases WHERE UserId = @UserId
|
||||
UNION ALL SELECT N'PaymentTransactions', COUNT(*) FROM CMS.PaymentTransactions WHERE UserId = @UserId OR Mobile = @UserMobile
|
||||
UNION ALL SELECT N'UserWallets', COUNT(*) FROM @Wallets
|
||||
UNION ALL SELECT N'UserWalletHistories', COUNT(*) FROM CMS.UserWalletHistories WHERE WalletId IN (SELECT WalletId FROM @Wallets)
|
||||
UNION ALL SELECT N'Transactions(collected)', COUNT(*) FROM @Tx
|
||||
UNION ALL SELECT N'NetworkChildren', @ChildCount
|
||||
UNION ALL SELECT N'UserOrders', COUNT(*) FROM @Orders
|
||||
UNION ALL SELECT N'DiscountOrders', COUNT(*) FROM @DiscountOrders
|
||||
UNION ALL SELECT N'UserCommissionPayouts', COUNT(*) FROM @Payouts
|
||||
UNION ALL SELECT N'PoolReversals(planned)', COUNT(*) FROM @PoolReversals WHERE Fee > 0;
|
||||
|
||||
SELECT N'--- Pool reverse plan ---' AS Info;
|
||||
SELECT * FROM @PoolReversals;
|
||||
|
||||
IF @ReversePool = 1 AND @AbortIfPoolCalculated = 1
|
||||
AND EXISTS (SELECT 1 FROM @PoolReversals WHERE IsCalculated = 1 AND Fee > 0)
|
||||
BEGIN
|
||||
RAISERROR(N'One or more target weekly pools are already calculated. Aborting (@AbortIfPoolCalculated=1).', 16, 1);
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
IF @DryRun = 1
|
||||
BEGIN
|
||||
PRINT N'DryRun=1 — no changes committed.';
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
BEGIN TRY
|
||||
/* ---- Reverse weekly pool charges FIRST (needs cycle + package data) ---- */
|
||||
IF @ReversePool = 1
|
||||
BEGIN
|
||||
UPDATE wp
|
||||
SET wp.TotalPoolAmount = CASE
|
||||
WHEN wp.TotalPoolAmount >= r.Fee THEN wp.TotalPoolAmount - r.Fee
|
||||
ELSE 0
|
||||
END,
|
||||
wp.LastModified = SYSUTCDATETIME()
|
||||
FROM CMS.WeeklyCommissionPools wp
|
||||
INNER JOIN (
|
||||
SELECT PoolId, SUM(Fee) AS Fee
|
||||
FROM @PoolReversals
|
||||
WHERE PoolId IS NOT NULL AND Fee > 0
|
||||
GROUP BY PoolId
|
||||
) r ON r.PoolId = wp.Id;
|
||||
|
||||
PRINT N'Pool reverse applied for ' + CAST(@@ROWCOUNT AS NVARCHAR(20)) + N' pool row(s).';
|
||||
END;
|
||||
|
||||
/* ---- Club ---- */
|
||||
DELETE h FROM CMS.ClubMembershipCycleHistories h
|
||||
WHERE h.UserId = @UserId OR h.ClubMembershipCycleId IN (SELECT CycleId FROM @Cycles);
|
||||
|
||||
DELETE FROM CMS.ClubMembershipCycles
|
||||
WHERE Id IN (SELECT CycleId FROM @Cycles) OR UserId = @UserId;
|
||||
|
||||
DELETE FROM CMS.ClubMembershipHistories WHERE UserId = @UserId
|
||||
OR ClubMembershipId IN (SELECT ClubMembershipId FROM @ClubMemberships);
|
||||
|
||||
DELETE FROM CMS.UserClubFeatures WHERE UserId = @UserId
|
||||
OR ClubMembershipId IN (SELECT ClubMembershipId FROM @ClubMemberships);
|
||||
|
||||
DELETE FROM CMS.ClubMemberships WHERE UserId = @UserId;
|
||||
|
||||
/* ---- Commission payouts of THIS user only ---- */
|
||||
DELETE h FROM CMS.CommissionPayoutHistories h
|
||||
WHERE h.UserId = @UserId OR h.UserCommissionPayoutId IN (SELECT PayoutId FROM @Payouts);
|
||||
|
||||
DELETE FROM CMS.UserCommissionPayouts WHERE UserId = @UserId;
|
||||
|
||||
/* ---- Network ---- */
|
||||
DELETE FROM CMS.NetworkWeeklyBalances WHERE UserId = @UserId;
|
||||
DELETE FROM CMS.NetworkMembershipHistories
|
||||
WHERE UserId = @UserId OR OldParentId = @UserId OR NewParentId = @UserId;
|
||||
|
||||
IF @ChildCount > 0 AND @ReparentChildren = 1
|
||||
UPDATE CMS.Users SET NetworkParentId = NULL, LastModified = SYSUTCDATETIME()
|
||||
WHERE NetworkParentId = @UserId;
|
||||
|
||||
/* ---- Invitations ---- */
|
||||
DELETE FROM CMS.UserInvitations
|
||||
WHERE InviterUserId = @UserId OR InviteeUserId = @UserId;
|
||||
|
||||
/* ---- Package / payment ---- */
|
||||
DELETE FROM CMS.UserPackagePurchases WHERE UserId = @UserId;
|
||||
DELETE FROM CMS.PaymentTransactions
|
||||
WHERE UserId = @UserId OR Mobile = @UserMobile OR Mobile = @MobileNorm;
|
||||
|
||||
/* ---- Manual / Daya ---- */
|
||||
DELETE FROM CMS.ManualPayments WHERE UserId = @UserId;
|
||||
DELETE FROM CMS.DayaLoanContracts WHERE UserId = @UserId;
|
||||
|
||||
/* ---- Discount shop ---- */
|
||||
DELETE d FROM CMS.DiscountOrderDetails d
|
||||
WHERE d.DiscountOrderId IN (SELECT DiscountOrderId FROM @DiscountOrders);
|
||||
DELETE FROM CMS.DiscountOrders WHERE UserId = @UserId;
|
||||
DELETE FROM CMS.DiscountShoppingCarts WHERE UserId = @UserId;
|
||||
|
||||
/* ---- Stock ---- */
|
||||
DELETE FROM CMS.StockMovements
|
||||
WHERE PerformedByUserId = @UserId
|
||||
OR OrderId IN (SELECT OrderId FROM @Orders)
|
||||
OR DiscountOrderId IN (SELECT DiscountOrderId FROM @DiscountOrders);
|
||||
|
||||
/* ---- Orders ---- */
|
||||
DELETE FROM CMS.FactorDetails WHERE OrderId IN (SELECT OrderId FROM @Orders);
|
||||
DELETE FROM CMS.OrderVATs WHERE OrderId IN (SELECT OrderId FROM @Orders);
|
||||
DELETE FROM CMS.UserOrders WHERE UserId = @UserId;
|
||||
|
||||
/* ---- Wallet ---- */
|
||||
DELETE FROM CMS.UserWalletHistories WHERE WalletId IN (SELECT WalletId FROM @Wallets);
|
||||
DELETE FROM CMS.UserWallets WHERE UserId = @UserId;
|
||||
|
||||
/* ---- Cart / address / roles / contracts ---- */
|
||||
DELETE FROM CMS.UserCarts WHERE UserId = @UserId;
|
||||
DELETE FROM CMS.UserAddresses WHERE UserId = @UserId;
|
||||
DELETE FROM CMS.UserRoles WHERE UserId = @UserId;
|
||||
DELETE FROM CMS.UserContracts WHERE UserId = @UserId;
|
||||
|
||||
/* ---- Content authored by test user ---- */
|
||||
UPDATE CMS.BlogPosts SET IsDeleted = 1, LastModified = SYSUTCDATETIME()
|
||||
WHERE AuthorUserId = @UserId AND IsDeleted = 0;
|
||||
|
||||
UPDATE CMS.PublicMessages SET IsDeleted = 1, LastModified = SYSUTCDATETIME()
|
||||
WHERE CreatedByUserId = @UserId AND IsDeleted = 0;
|
||||
|
||||
/* ---- Transactions ---- */
|
||||
DELETE t
|
||||
FROM CMS.Transactions t
|
||||
WHERE t.Id IN (SELECT TransactionId FROM @Tx)
|
||||
AND NOT EXISTS (SELECT 1 FROM CMS.UserPackagePurchases p WHERE p.TransactionId = t.Id)
|
||||
AND NOT EXISTS (SELECT 1 FROM CMS.PaymentTransactions p WHERE p.TransactionId = t.Id)
|
||||
AND NOT EXISTS (SELECT 1 FROM CMS.UserOrders o WHERE o.TransactionId = t.Id)
|
||||
AND NOT EXISTS (SELECT 1 FROM CMS.ManualPayments m WHERE m.TransactionId = t.Id)
|
||||
AND NOT EXISTS (SELECT 1 FROM CMS.DayaLoanContracts d WHERE d.TransactionId = t.Id)
|
||||
AND NOT EXISTS (SELECT 1 FROM CMS.DiscountOrders d WHERE d.TransactionId = t.Id);
|
||||
|
||||
/* ---- OTP ---- */
|
||||
DELETE FROM CMS.OtpTokens
|
||||
WHERE Mobile IN (@UserMobile, @MobileNorm, ISNULL(@Mobile, N''));
|
||||
|
||||
/* ---- User ---- */
|
||||
IF @HardDeleteUser = 1
|
||||
DELETE FROM CMS.Users WHERE Id = @UserId;
|
||||
ELSE
|
||||
UPDATE CMS.Users
|
||||
SET IsDeleted = 1, LastModified = SYSUTCDATETIME()
|
||||
WHERE Id = @UserId;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
|
||||
PRINT N'Cleanup completed successfully.';
|
||||
SELECT N'DONE' AS Status, @UserId AS UserId, @UserMobile AS Mobile,
|
||||
@HardDeleteUser AS HardDeleted, @ReversePool AS PoolReversed;
|
||||
END TRY
|
||||
BEGIN CATCH
|
||||
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
|
||||
DECLARE @Err NVARCHAR(4000) = ERROR_MESSAGE();
|
||||
RAISERROR(N'Cleanup failed: %s', 16, 1, @Err);
|
||||
END CATCH
|
||||
END;
|
||||
GO
|
||||
@@ -15,6 +15,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.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)
|
||||
};
|
||||
}
|
||||
}
|
||||
+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);
|
||||
|
||||
@@ -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
-8
@@ -8,6 +8,7 @@ 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;
|
||||
@@ -16,20 +17,23 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
|
||||
: IRequestHandler<VerifyUserPackagePurchasePaymentCommand, VerifyUserPackagePurchasePaymentResult>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IPaymentGatewayService _paymentGateway;
|
||||
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,
|
||||
IPaymentGatewayService paymentGateway,
|
||||
IPaymentGatewayResolver paymentGatewayResolver,
|
||||
IConfiguration configuration,
|
||||
ISender sender,
|
||||
IUserPaymentLock paymentLock,
|
||||
ILogger<VerifyUserPackagePurchasePaymentCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_paymentGateway = paymentGateway;
|
||||
_paymentGatewayResolver = paymentGatewayResolver;
|
||||
_configuration = configuration;
|
||||
_sender = sender;
|
||||
_paymentLock = paymentLock;
|
||||
_logger = logger;
|
||||
@@ -105,7 +109,8 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
|
||||
}
|
||||
|
||||
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
|
||||
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
|
||||
var gateway = _paymentGatewayResolver.Get(paymentTx?.GatewayProvider);
|
||||
var verifyResult = await gateway.VerifyPaymentAsync(
|
||||
request.Authority, request.Status, (decimal)amountInToman, ct);
|
||||
|
||||
if (!verifyResult.IsSuccess)
|
||||
@@ -193,17 +198,30 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
var discountMultiplier = purchase.Package?.DiscountMultiplier
|
||||
var packagePrice = purchase.Package?.Price
|
||||
?? throw new InvalidOperationException("پکیج سفارش یافت نشد");
|
||||
var discountAmount = (long)(purchase.Amount * (double)discountMultiplier);
|
||||
wallet.Balance += purchase.Amount;
|
||||
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 = purchase.Amount,
|
||||
ChangeValue = balanceCredit,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace CMSMicroservice.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// رابطه دعوتکننده ↔ دعوتشونده (جدا از معرف شبکه / NetworkParent).
|
||||
/// کد دعوت = شماره موبایل دعوتکننده.
|
||||
/// </summary>
|
||||
public class UserInvitation : BaseAuditableEntity
|
||||
{
|
||||
/// <summary>کاربری که دعوت کرده</summary>
|
||||
public long InviterUserId { get; set; }
|
||||
|
||||
public virtual User Inviter { get; set; } = null!;
|
||||
|
||||
/// <summary>کاربری که دعوت شده (حداکثر یک دعوتکننده)</summary>
|
||||
public long InviteeUserId { get; set; }
|
||||
|
||||
public virtual User Invitee { get; set; } = null!;
|
||||
}
|
||||
@@ -15,5 +15,7 @@ public enum DeliveryStatus
|
||||
Returned = 4,
|
||||
// لغو شده
|
||||
Cancelled = 5,
|
||||
// آماده تحویل حضوری در دفتر
|
||||
ReadyForOfficePickup = 6,
|
||||
}
|
||||
|
||||
|
||||
@@ -18,5 +18,10 @@ public enum PackagePurchaseMethod
|
||||
/// <summary>
|
||||
/// از طریق پرداخت مستقیم درگاه بانکی
|
||||
/// </summary>
|
||||
DirectPurchase = 2
|
||||
DirectPurchase = 2,
|
||||
|
||||
/// <summary>
|
||||
/// ثبت دستی توسط ادمین
|
||||
/// </summary>
|
||||
Manual = 3
|
||||
}
|
||||
|
||||
@@ -36,4 +36,14 @@ public enum TransactionType
|
||||
/// بونوس داخلی کیفپول جادویی (مبلغ × 1.5)
|
||||
/// </summary>
|
||||
MagicWalletBonus = 15,
|
||||
|
||||
/// <summary>
|
||||
/// شارژ کیف پول اصلی از درگاه
|
||||
/// </summary>
|
||||
CreditWalletCharge = 16,
|
||||
|
||||
/// <summary>
|
||||
/// شارژ دستی کیف پول اصلی توسط ادمین
|
||||
/// </summary>
|
||||
ManualCreditWalletCharge = 17,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePa
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
|
||||
using CMSMicroservice.Application.WalletCQ.Commands.VerifyDiscountWalletCharge;
|
||||
using CMSMicroservice.Application.WalletCQ.Commands.VerifyCreditWalletCharge;
|
||||
using CMSMicroservice.Application.WalletCQ.Commands.VerifyMagicWalletCharge;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Entities.Payment;
|
||||
@@ -174,6 +175,19 @@ public class ZarinpalReconciliationJob
|
||||
}, ct);
|
||||
break;
|
||||
|
||||
case "credit-wallet":
|
||||
if (!paymentTx.UserId.HasValue)
|
||||
throw new InvalidOperationException(
|
||||
$"UserId is null for credit-wallet authority {authority}");
|
||||
|
||||
await _sender.Send(new VerifyCreditWalletChargeCommand
|
||||
{
|
||||
Authority = authority,
|
||||
UserId = paymentTx.UserId.Value,
|
||||
Amount = paymentTx.Amount
|
||||
}, ct);
|
||||
break;
|
||||
|
||||
case "discount-order":
|
||||
await ReconcileDiscountOrderAsync(paymentTx, ct);
|
||||
break;
|
||||
@@ -194,6 +208,8 @@ public class ZarinpalReconciliationJob
|
||||
return "magic-wallet";
|
||||
if (callbackUrl.Contains("type=discount-wallet", StringComparison.OrdinalIgnoreCase))
|
||||
return "discount-wallet";
|
||||
if (callbackUrl.Contains("type=credit-wallet", StringComparison.OrdinalIgnoreCase))
|
||||
return "credit-wallet";
|
||||
if (callbackUrl.Contains("type=discount-order", StringComparison.OrdinalIgnoreCase))
|
||||
return "discount-order";
|
||||
// Package callbacks have orderId= but no type= parameter
|
||||
|
||||
@@ -77,27 +77,33 @@ public static class ConfigureServices
|
||||
});
|
||||
}
|
||||
|
||||
// Payment Gateway Service - Multi-Provider Architecture
|
||||
// پشتیبانی از درگاههای مختلف: ZarinPal, Daya, Mock
|
||||
// Payment Gateway — default provider + multi-gateway resolver (ZarinPal + TwentyCard coexist)
|
||||
var paymentProvider = configuration.GetValue<string>("PaymentProvider", "Mock")?.ToLowerInvariant();
|
||||
|
||||
|
||||
services.AddHttpClient<ZarinPalPaymentService>()
|
||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||
services.AddHttpClient<TwentyCardPaymentService>()
|
||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||
|
||||
switch (paymentProvider)
|
||||
{
|
||||
case "zarinpal":
|
||||
services.AddHttpClient<IPaymentGatewayService, ZarinPalPaymentService>()
|
||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||
services.AddScoped<IPaymentGatewayService>(sp => sp.GetRequiredService<ZarinPalPaymentService>());
|
||||
break;
|
||||
|
||||
|
||||
case "daya":
|
||||
services.AddHttpClient<IPaymentGatewayService, DayaPaymentService>()
|
||||
services.AddHttpClient<DayaPaymentService>()
|
||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||
services.AddScoped<IPaymentGatewayService>(sp => sp.GetRequiredService<DayaPaymentService>());
|
||||
break;
|
||||
|
||||
|
||||
case "mock":
|
||||
default:
|
||||
services.AddScoped<IPaymentGatewayService, MockPaymentGatewayService>();
|
||||
break;
|
||||
}
|
||||
|
||||
services.AddScoped<IPaymentGatewayResolver, PaymentGatewayResolver>();
|
||||
|
||||
services.AddScoped<IApplicationDbContext>(p => p.GetRequiredService<ApplicationDbContext>());
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
|
||||
public DbSet<Product> Products => Set<Product>();
|
||||
public DbSet<ProductImage> ProductImages => Set<ProductImage>();
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<UserInvitation> UserInvitations => Set<UserInvitation>();
|
||||
public DbSet<OtpToken> OtpTokens => Set<OtpToken>();
|
||||
public DbSet<Contract> Contracts => Set<Contract>();
|
||||
public DbSet<UserContract> UserContracts => Set<UserContract>();
|
||||
|
||||
@@ -13,7 +13,7 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
builder.Property(entity => entity.Id).UseIdentityColumn();
|
||||
builder.Property(entity => entity.FirstName).IsRequired(false);
|
||||
builder.Property(entity => entity.LastName).IsRequired(false);
|
||||
builder.Property(entity => entity.Mobile).IsRequired(true);
|
||||
builder.Property(entity => entity.Mobile).IsRequired(true).HasMaxLength(20);
|
||||
builder.Property(entity => entity.NationalCode).IsRequired(false);
|
||||
builder.Property(entity => entity.AvatarPath).IsRequired(false);
|
||||
builder.Property(entity => entity.ReferralCode).IsRequired(true);
|
||||
@@ -48,5 +48,10 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
builder.HasIndex(e => e.LegPosition)
|
||||
.HasDatabaseName("IX_User_LegPosition");
|
||||
|
||||
// موبایل یکتا برای کاربران فعال (lookup دعوتکننده)
|
||||
builder.HasIndex(e => e.Mobile)
|
||||
.IsUnique()
|
||||
.HasFilter("[IsDeleted] = 0")
|
||||
.HasDatabaseName("IX_User_Mobile_Active");
|
||||
}
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class UserInvitationConfiguration : IEntityTypeConfiguration<UserInvitation>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UserInvitation> builder)
|
||||
{
|
||||
builder.ToTable("UserInvitations");
|
||||
builder.HasQueryFilter(p => !p.IsDeleted);
|
||||
builder.Ignore(entity => entity.DomainEvents);
|
||||
|
||||
builder.HasKey(entity => entity.Id);
|
||||
builder.Property(entity => entity.Id).UseIdentityColumn();
|
||||
|
||||
builder.Property(entity => entity.InviterUserId).IsRequired();
|
||||
builder.Property(entity => entity.InviteeUserId).IsRequired();
|
||||
|
||||
builder.HasOne(entity => entity.Inviter)
|
||||
.WithMany()
|
||||
.HasForeignKey(entity => entity.InviterUserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(entity => entity.Invitee)
|
||||
.WithMany()
|
||||
.HasForeignKey(entity => entity.InviteeUserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(e => e.InviteeUserId)
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_UserInvitations_InviteeUserId");
|
||||
|
||||
builder.HasIndex(e => new { e.InviterUserId, e.Created })
|
||||
.HasDatabaseName("IX_UserInvitations_InviterUserId_Created");
|
||||
}
|
||||
}
|
||||
+5269
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user