Compare commits
128 Commits
b4e7902870
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 5c1e3cf601 | |||
| 37820ea257 | |||
| 5f8e30905d | |||
| d82b7c28a8 | |||
| 02b2966e96 | |||
| 29fa7ff435 | |||
| 3df738e496 | |||
| dba0b63a70 | |||
| 3a9c3438e6 | |||
| 541b0c2ce9 | |||
| 12ebdc9e87 | |||
| 121291eeed | |||
| 9f1bc2bcdb | |||
| d22eb1617f | |||
| f08d3ac78f | |||
| 354ba1dd1b | |||
| 4f2d313d5e | |||
| 034679d915 | |||
| 3b9958a700 | |||
| 9c562b12c7 | |||
| 6414e5663a | |||
| 3c9957463d | |||
| 90d4f65de4 | |||
| 4df71107a9 | |||
| bc52240a41 | |||
| a7fc959d3e | |||
| aac0d4271d | |||
| d96558aa4b | |||
| da1a2fcf8e | |||
| a2d13c72c3 | |||
| 4e8caa6e2c | |||
| cdd124ac25 | |||
| 4d7932f335 | |||
| 18a2299d93 | |||
| 37a017998c | |||
| 6e60ca7c25 | |||
| fd8b37a6b1 | |||
| 683ed370b5 | |||
| 16acb8f3b5 | |||
| 98c2875ae2 | |||
| 34d2354fd4 | |||
| 65db839642 | |||
| f7d6b0e3e7 | |||
| be2b22c50a | |||
| b905c308c1 | |||
| d09db638cc | |||
| 41e7f29a0d | |||
| c4409491c1 | |||
| e4279f3d05 | |||
| b260fd56fa | |||
| c5dd1a2ef5 | |||
| 1e3ba03c5c | |||
| eeb837f32e | |||
| ee7d29015f | |||
| 624ab7ea9f | |||
| d8a3b49f4d | |||
| b053d86006 | |||
| 15044c66cb | |||
| 8518c7b3ec | |||
| 0983cdf88f | |||
| 789aa0b870 | |||
| b8120dd1b9 | |||
| 7ce5539793 | |||
| 9a4f1e6401 | |||
| 091a04f3aa | |||
| 62a0f8594d | |||
| fb81aff095 | |||
| 491b8a0d0f | |||
| be80136ae8 | |||
| 5e9984f0e5 | |||
| 1d07ae4f71 | |||
| 775a9bd75d | |||
| 2620a24597 | |||
| def3b94d89 | |||
| 3ebe0f9f48 | |||
| f4e24f7449 | |||
| 540a2ce4fc | |||
| e57f01965a | |||
| f97c4b0812 | |||
| fd32e15192 | |||
| 4456e504d4 | |||
| cec1e74f6c | |||
| dd9ac48474 | |||
| eb1b249869 | |||
| e8e964f9a0 | |||
| 9050c00297 | |||
| 6048824b33 | |||
| 62a247f097 | |||
| af12f996f1 | |||
| 3b0d9eadd3 | |||
| db07838ecb | |||
| 167991890c | |||
| 1a425f0d93 | |||
| 6f8aefc2ce | |||
| 2bd5c7db78 | |||
| 0728ec2b76 | |||
| 1425fb187b | |||
| 81d2b39ee1 | |||
| fce194a195 |
@@ -1,46 +0,0 @@
|
|||||||
|
|
||||||
name: Push nuget and docker image Actions Workflow
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- stage_new
|
|
||||||
jobs:
|
|
||||||
Deploy:
|
|
||||||
runs-on: windows
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: https://git.afrino.co/actions/checkout@v3
|
|
||||||
- name: Setup dotnet
|
|
||||||
uses: https://git.afrino.co/actions/setup-dotnet@v3
|
|
||||||
with:
|
|
||||||
dotnet-version: 7.0.x
|
|
||||||
|
|
||||||
- name: Remove Package Source
|
|
||||||
run: dotnet nuget remove source FourSat
|
|
||||||
continue-on-error: true
|
|
||||||
- name: Add Package Source
|
|
||||||
run: dotnet nuget add source --name FourSat --username systemuser --password sZSA7PTiv3pUSQZ https://git.afrino.co/api/packages/FourSat/nuget/index.json --store-password-in-clear-text
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: dotnet restore ".\src\CMSMicroservice.WebApi\CMSMicroservice.WebApi.csproj"
|
|
||||||
- name: Build
|
|
||||||
run: dotnet build ".\src\CMSMicroservice.WebApi\CMSMicroservice.WebApi.csproj" --configuration Release --no-restore
|
|
||||||
- name: Test
|
|
||||||
run: dotnet test ".\src\CMSMicroservice.WebApi\CMSMicroservice.WebApi.csproj" --no-restore --verbosity normal
|
|
||||||
- name: Recycle Apppool
|
|
||||||
run: |
|
|
||||||
& "C:\Windows\System32\inetsrv\appcmd.exe" recycle apppool /apppool.name:cms.kbs1.ir
|
|
||||||
shell: powershell
|
|
||||||
- name: Stop Website
|
|
||||||
run: |
|
|
||||||
& "C:\Windows\System32\inetsrv\appcmd.exe" stop site /site.name:cms.kbs1.ir
|
|
||||||
shell: powershell
|
|
||||||
- name: Publish
|
|
||||||
run: dotnet publish ".\src\CMSMicroservice.WebApi\CMSMicroservice.WebApi.csproj" -c Release -o publish
|
|
||||||
- name: Copy Publish To IIS Directory
|
|
||||||
run: Get-ChildItem -Path "publish\*" | Copy-Item -Destination "E:\kbs1.ir\cms.kbs1.ir\" -Recurse -Force
|
|
||||||
- name: Start Website
|
|
||||||
run: |
|
|
||||||
& "C:\Windows\System32\inetsrv\appcmd.exe" start site /site.name:cms.kbs1.ir
|
|
||||||
shell: powershell
|
|
||||||
@@ -95,6 +95,7 @@ jobs:
|
|||||||
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
||||||
kubectl apply -f /tmp/cms-config.yaml &&
|
kubectl apply -f /tmp/cms-config.yaml &&
|
||||||
kubectl apply -f /tmp/cms-deployment.yaml &&
|
kubectl apply -f /tmp/cms-deployment.yaml &&
|
||||||
|
crictl rmi ${REGISTRY}/${IMAGE_NAME}:latest 2>/dev/null || true &&
|
||||||
kubectl rollout restart deployment/cms &&
|
kubectl rollout restart deployment/cms &&
|
||||||
kubectl rollout status deployment/cms --timeout=180s &&
|
kubectl rollout status deployment/cms --timeout=180s &&
|
||||||
rm -f /tmp/cms-config.yaml /tmp/cms-deployment.yaml
|
rm -f /tmp/cms-config.yaml /tmp/cms-deployment.yaml
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
if ! docker info >/dev/null 2>&1; then
|
if ! docker info >/dev/null 2>&1; then
|
||||||
echo "❌ Docker daemon failed to start after 3 minutes"
|
echo "❌ Docker daemon failed to start"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -75,8 +75,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
run: |
|
run: |
|
||||||
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
DOCKER_BUILDKIT=0 docker build --network host \
|
||||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod .
|
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
||||||
|
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
|
||||||
|
.
|
||||||
|
|
||||||
- name: Push to Registry
|
- name: Push to Registry
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
# CMS Microservice
|
# CMS Microservice
|
||||||
|
|
||||||
Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
|
Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
|
||||||
|
|
||||||
@@ -24,6 +24,8 @@ stringData:
|
|||||||
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
||||||
"UseSandbox": true
|
"UseSandbox": true
|
||||||
},
|
},
|
||||||
|
"CmsBaseUrl": "https://cms.se.kbs1.ir",
|
||||||
|
"FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir",
|
||||||
"FMS": {
|
"FMS": {
|
||||||
"Address": "https://dl.afrino.co"
|
"Address": "https://dl.afrino.co"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ spec:
|
|||||||
|
|
||||||
---
|
---
|
||||||
# CMS Ingress
|
# CMS Ingress
|
||||||
|
# If clients report TLS PartialChain, the tls secret (cms-tls) tls.crt must include the full chain
|
||||||
|
# (leaf + intermediates). cert-manager ACME usually provides this; manual certs need fullchain.pem in tls.crt.
|
||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
|
|||||||
+5
-3
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
using CMSMicroservice.Application.Common.FileManager;
|
using CMSMicroservice.Application.Common.FileManager;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities.Blog;
|
using CMSMicroservice.Domain.Entities.Blog;
|
||||||
@@ -32,12 +33,13 @@ public class CreateBlogPostCommandHandler : IRequestHandler<CreateBlogPostComman
|
|||||||
if (string.IsNullOrEmpty(currentUserId) || !long.TryParse(currentUserId, out var authorUserId))
|
if (string.IsNullOrEmpty(currentUserId) || !long.TryParse(currentUserId, out var authorUserId))
|
||||||
throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
|
throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
|
||||||
|
|
||||||
|
var slug = BlogSlugHelper.ResolveForCreate(request.Slug, request.Title.Trim());
|
||||||
|
slug = await BlogSlugHelper.EnsureUniquePostSlugAsync(_context, slug, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
var post = new BlogPost
|
var post = new BlogPost
|
||||||
{
|
{
|
||||||
Title = request.Title.Trim(),
|
Title = request.Title.Trim(),
|
||||||
Slug = string.IsNullOrWhiteSpace(request.Slug)
|
Slug = slug,
|
||||||
? $"post-{Guid.NewGuid():N}".Substring(0, 20)
|
|
||||||
: request.Slug.Trim().ToLower(),
|
|
||||||
Summary = request.Summary?.Trim(),
|
Summary = request.Summary?.Trim(),
|
||||||
HtmlContent = request.HtmlContent,
|
HtmlContent = request.HtmlContent,
|
||||||
FeaturedImagePath = request.FeaturedImagePath,
|
FeaturedImagePath = request.FeaturedImagePath,
|
||||||
|
|||||||
+2
-2
@@ -11,9 +11,9 @@ public class CreateBlogPostCommandValidator : AbstractValidator<CreateBlogPostCo
|
|||||||
.MaximumLength(200).WithMessage("عنوان نمیتواند بیشتر از 200 کاراکتر باشد");
|
.MaximumLength(200).WithMessage("عنوان نمیتواند بیشتر از 200 کاراکتر باشد");
|
||||||
|
|
||||||
RuleFor(x => x.Slug)
|
RuleFor(x => x.Slug)
|
||||||
.NotEmpty().WithMessage("نشانی یکتا (slug) الزامی است")
|
|
||||||
.MaximumLength(200).WithMessage("نشانی نمیتواند بیشتر از 200 کاراکتر باشد")
|
.MaximumLength(200).WithMessage("نشانی نمیتواند بیشتر از 200 کاراکتر باشد")
|
||||||
.Matches(@"^[a-z0-9\-]+$").WithMessage("نشانی فقط میتواند شامل حروف کوچک انگلیسی، اعداد و خط تیره باشد");
|
.Matches(@"^[a-z0-9\-]+$").WithMessage("نشانی فقط میتواند شامل حروف کوچک انگلیسی، اعداد و خط تیره باشد")
|
||||||
|
.When(x => !string.IsNullOrWhiteSpace(x.Slug));
|
||||||
|
|
||||||
RuleFor(x => x.Summary)
|
RuleFor(x => x.Summary)
|
||||||
.MaximumLength(500).WithMessage("خلاصه نمیتواند بیشتر از 500 کاراکتر باشد")
|
.MaximumLength(500).WithMessage("خلاصه نمیتواند بیشتر از 500 کاراکتر باشد")
|
||||||
|
|||||||
+15
-2
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
using CMSMicroservice.Application.Common.FileManager;
|
using CMSMicroservice.Application.Common.FileManager;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities.Blog;
|
using CMSMicroservice.Domain.Entities.Blog;
|
||||||
@@ -29,9 +30,21 @@ public class UpdateBlogPostCommandHandler : IRequestHandler<UpdateBlogPostComman
|
|||||||
?? throw new KeyNotFoundException($"مقاله با شناسه {request.Id} یافت نشد");
|
?? throw new KeyNotFoundException($"مقاله با شناسه {request.Id} یافت نشد");
|
||||||
|
|
||||||
post.Title = request.Title.Trim();
|
post.Title = request.Title.Trim();
|
||||||
// حفظ slug قبلی اگر مقدار جدید خالی باشد
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.Slug))
|
if (!string.IsNullOrWhiteSpace(request.Slug))
|
||||||
post.Slug = request.Slug.Trim().ToLower();
|
{
|
||||||
|
post.Slug = await BlogSlugHelper.EnsureUniquePostSlugAsync(
|
||||||
|
_context,
|
||||||
|
BlogSlugHelper.NormalizeOptional(request.Slug)!,
|
||||||
|
post.Id,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrWhiteSpace(post.Slug))
|
||||||
|
{
|
||||||
|
var slug = BlogSlugHelper.ResolveForCreate(null, post.Title);
|
||||||
|
post.Slug = await BlogSlugHelper.EnsureUniquePostSlugAsync(
|
||||||
|
_context, slug, post.Id, cancellationToken);
|
||||||
|
}
|
||||||
post.Summary = request.Summary?.Trim();
|
post.Summary = request.Summary?.Trim();
|
||||||
post.HtmlContent = request.HtmlContent;
|
post.HtmlContent = request.HtmlContent;
|
||||||
post.FeaturedImagePath = request.FeaturedImagePath;
|
post.FeaturedImagePath = request.FeaturedImagePath;
|
||||||
|
|||||||
+2
-2
@@ -13,9 +13,9 @@ public class UpdateBlogPostCommandValidator : AbstractValidator<UpdateBlogPostCo
|
|||||||
.MaximumLength(200).WithMessage("عنوان نمیتواند بیشتر از 200 کاراکتر باشد");
|
.MaximumLength(200).WithMessage("عنوان نمیتواند بیشتر از 200 کاراکتر باشد");
|
||||||
|
|
||||||
RuleFor(x => x.Slug)
|
RuleFor(x => x.Slug)
|
||||||
.NotEmpty().WithMessage("نشانی یکتا (slug) الزامی است")
|
|
||||||
.MaximumLength(200).WithMessage("نشانی نمیتواند بیشتر از 200 کاراکتر باشد")
|
.MaximumLength(200).WithMessage("نشانی نمیتواند بیشتر از 200 کاراکتر باشد")
|
||||||
.Matches(@"^[a-z0-9\-]+$").WithMessage("نشانی فقط میتواند شامل حروف کوچک انگلیسی، اعداد و خط تیره باشد");
|
.Matches(@"^[a-z0-9\-]+$").WithMessage("نشانی فقط میتواند شامل حروف کوچک انگلیسی، اعداد و خط تیره باشد")
|
||||||
|
.When(x => !string.IsNullOrWhiteSpace(x.Slug));
|
||||||
|
|
||||||
RuleFor(x => x.HtmlContent)
|
RuleFor(x => x.HtmlContent)
|
||||||
.NotEmpty().WithMessage("محتوای مقاله الزامی است");
|
.NotEmpty().WithMessage("محتوای مقاله الزامی است");
|
||||||
|
|||||||
+102
-5
@@ -135,15 +135,65 @@ public class AcceptClubMembershipContractCommandHandler
|
|||||||
};
|
};
|
||||||
await _context.UserContracts.AddAsync(userContract, cancellationToken);
|
await _context.UserContracts.AddAsync(userContract, cancellationToken);
|
||||||
|
|
||||||
// 6. بارگذاری پکیج از آخرین خرید کاربر یا پکیج پایه
|
// 6. بارگذاری پکیج موثر برای امضای قرارداد
|
||||||
|
// اولویت:
|
||||||
|
// 1) پکیج Cycle جاری (مسیر صحیح برای پرداخت دستی)
|
||||||
|
// 2) آخرین خرید پکیج
|
||||||
|
// 3) پکیج پایه
|
||||||
|
var currentCyclePackageId = await _context.ClubMembershipCycles
|
||||||
|
.Where(c => c.UserId == userId && c.IsCurrentCycle)
|
||||||
|
.OrderByDescending(c => c.CycleNumber)
|
||||||
|
.Select(c => (long?)c.PackageId)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
var latestPurchase = await _context.UserPackagePurchases
|
var latestPurchase = await _context.UserPackagePurchases
|
||||||
.Where(p => p.UserId == userId)
|
.Where(p => p.UserId == userId)
|
||||||
.OrderByDescending(p => p.PurchasedAt)
|
.OrderByDescending(p => p.PurchasedAt)
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
var package = latestPurchase != null
|
Package? package = null;
|
||||||
? await _context.Packages.FirstOrDefaultAsync(p => p.Id == latestPurchase.PackageId && !p.IsDeleted, cancellationToken)
|
|
||||||
: await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken);
|
if (currentCyclePackageId.HasValue && currentCyclePackageId.Value > 0)
|
||||||
|
{
|
||||||
|
package = await _context.Packages
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == currentCyclePackageId.Value && !p.IsDeleted, cancellationToken);
|
||||||
|
|
||||||
|
if (package != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"AcceptContract package source: CurrentCycle (UserId={UserId}, PackageId={PackageId})",
|
||||||
|
userId,
|
||||||
|
package.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (package == null && latestPurchase != null)
|
||||||
|
{
|
||||||
|
package = await _context.Packages
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == latestPurchase.PackageId && !p.IsDeleted, cancellationToken);
|
||||||
|
|
||||||
|
if (package != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"AcceptContract package source: LatestPurchase (UserId={UserId}, PackageId={PackageId})",
|
||||||
|
userId,
|
||||||
|
package.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (package == null)
|
||||||
|
{
|
||||||
|
package = await _context.Packages
|
||||||
|
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken);
|
||||||
|
|
||||||
|
if (package != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"AcceptContract package source: BasePackage (UserId={UserId}, PackageId={PackageId})",
|
||||||
|
userId,
|
||||||
|
package.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (package == null)
|
if (package == null)
|
||||||
throw new NotFoundException("پکیج یافت نشد");
|
throw new NotFoundException("پکیج یافت نشد");
|
||||||
@@ -221,7 +271,54 @@ public class AcceptClubMembershipContractCommandHandler
|
|||||||
_context.ClubMembershipHistories.Add(history);
|
_context.ClubMembershipHistories.Add(history);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 9. اضافه کردن مبلغ به Pool هفته جاری
|
// 8.5. آپدیت PackagePurchasedAt سایکل جاری به زمان امضای قرارداد
|
||||||
|
// دلیل: Pool در همین لحظه شارژ میشود — Validation باید به همین هفته اشاره کند
|
||||||
|
// سناریوی cross-week: کاربر هفته ۱۸ پکیج خریده، هفته ۱۹ قرارداد امضا کرده
|
||||||
|
// بدون این Fix: Cycle.PackagePurchasedAt = هفته ۱۸ ولی Pool هفته ۱۹ → اختلاف
|
||||||
|
// با این Fix: Cycle.PackagePurchasedAt = هفته ۱۹ = Pool هفته ۱۹ → درست
|
||||||
|
var currentCycle = await _context.ClubMembershipCycles
|
||||||
|
.FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, cancellationToken);
|
||||||
|
|
||||||
|
if (currentCycle != null)
|
||||||
|
{
|
||||||
|
currentCycle.PackagePurchasedAt = activationDate;
|
||||||
|
_context.ClubMembershipCycles.Update(currentCycle);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Updated Cycle #{CycleNumber} PackagePurchasedAt to contract-signing time for UserId {UserId}",
|
||||||
|
currentCycle.CycleNumber,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// در صورتی که Cycle وجود ندارد (legacy یا مسیر مستقیم AcceptContract)
|
||||||
|
// Cycle را همینجا بساز تا Validation درست کار کند
|
||||||
|
_logger.LogWarning(
|
||||||
|
"No IsCurrentCycle found for UserId {UserId} at AcceptContract — creating Cycle now",
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
var maxCycleNumber = await _context.ClubMembershipCycles
|
||||||
|
.Where(c => c.ClubMembershipId == clubMembership.Id)
|
||||||
|
.MaxAsync(c => (int?)c.CycleNumber, cancellationToken) ?? 0;
|
||||||
|
|
||||||
|
var newCycle = new ClubMembershipCycle
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
ClubMembershipId = clubMembership.Id,
|
||||||
|
CycleNumber = maxCycleNumber + 1,
|
||||||
|
PackagePurchasedAt = activationDate,
|
||||||
|
PurchaseMethod = user.PackagePurchaseMethod,
|
||||||
|
PackageAmount = package.Price,
|
||||||
|
PackageId = package.Id,
|
||||||
|
IsCurrentCycle = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.ClubMembershipCycles.Add(newCycle);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
|
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
|
||||||
var weeklyPool = await _context.WeeklyCommissionPools
|
var weeklyPool = await _context.WeeklyCommissionPools
|
||||||
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
|
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
|
||||||
|
|||||||
+6
@@ -17,4 +17,10 @@ public record ActivateClubMembershipCommand : IRequest<bool>
|
|||||||
/// فعالسازی اجباری توسط ادمین — بدون بررسی سفارش و کیفپول
|
/// فعالسازی اجباری توسط ادمین — بدون بررسی سفارش و کیفپول
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ForceActivation { get; init; }
|
public bool ForceActivation { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه پکیج — فقط در ForceActivation استفاده میشود.
|
||||||
|
/// اگر 0 باشد، پکیج پایه (IsBasePackage) استفاده میشود.
|
||||||
|
/// </summary>
|
||||||
|
public long PackageId { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-9
@@ -1,4 +1,5 @@
|
|||||||
using CMSMicroservice.Application.Common.Exceptions;
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
|
using CMSMicroservice.Application.Common.Helpers;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Application.Common.Models;
|
using CMSMicroservice.Application.Common.Models;
|
||||||
using CMSMicroservice.Domain.Common;
|
using CMSMicroservice.Domain.Common;
|
||||||
@@ -162,19 +163,28 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
request.UserId
|
request.UserId
|
||||||
);
|
);
|
||||||
|
|
||||||
// در فعالسازی اجباری، اگر PackagePurchaseMethod تنظیم نشده، مقدار DirectPurchase بگذار
|
// در فعالسازی اجباری ادمین، اگر روش خرید تنظیم نشده، دستی در نظر بگیر
|
||||||
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
|
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
|
||||||
{
|
{
|
||||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
user.PackagePurchaseMethod = PackagePurchaseMethod.Manual;
|
||||||
_context.Users.Update(user);
|
_context.Users.Update(user);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// بارگذاری پکیج پایه برای فعالسازی اجباری
|
// بارگذاری پکیج — اگر PackageId مشخص شده همان را لود کن، وگرنه پکیج پایه
|
||||||
|
if (request.PackageId > 0)
|
||||||
|
{
|
||||||
|
package = await _context.Packages
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == request.PackageId && !p.IsDeleted, cancellationToken)
|
||||||
|
?? throw new NotFoundException($"پکیج {request.PackageId} یافت نشد");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
package = await _context.Packages
|
package = await _context.Packages
|
||||||
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
|
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
|
||||||
?? throw new NotFoundException("پکیج پایه یافت نشد");
|
?? throw new NotFoundException("پکیج پایه یافت نشد");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 6. بررسی عضویت فعلی
|
// 6. بررسی عضویت فعلی
|
||||||
var existingMembership = await _context.ClubMemberships
|
var existingMembership = await _context.ClubMemberships
|
||||||
@@ -226,10 +236,12 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
var hasCurrentCycle = await _context.ClubMembershipCycles
|
var hasCurrentCycle = await _context.ClubMembershipCycles
|
||||||
.AnyAsync(c => c.UserId == user.Id && c.IsCurrentCycle, cancellationToken);
|
.AnyAsync(c => c.UserId == user.Id && c.IsCurrentCycle, cancellationToken);
|
||||||
|
|
||||||
if (existingMembership.IsActive && hasCurrentCycle)
|
// اگر چرخه فعال دارد و درخواست Force نیست — احتمال اجرای تکراری همان خرید
|
||||||
|
// در حالت ForceActivation (مثل پرداخت دستی)، باید چرخه جدید ساخته شود تا پکیج جدید ثبت شود.
|
||||||
|
if (hasCurrentCycle && !request.ForceActivation)
|
||||||
{
|
{
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"User {UserId} is already an active club member with a running cycle",
|
"User {UserId} already has a running cycle — skipping duplicate activation",
|
||||||
user.Id
|
user.Id
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
@@ -261,6 +273,7 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
foreach (var prevCycle in previousCycles)
|
foreach (var prevCycle in previousCycles)
|
||||||
{
|
{
|
||||||
prevCycle.IsCurrentCycle = false;
|
prevCycle.IsCurrentCycle = false;
|
||||||
|
ClubMembershipCycleHistoryWriter.WriteCycleClosed(_context, prevCycle, reason: "شروع دوره جدید");
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxCycleNumber = await _context.ClubMembershipCycles
|
var maxCycleNumber = await _context.ClubMembershipCycles
|
||||||
@@ -282,6 +295,9 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
_context.ClubMembershipCycles.Add(newCycle);
|
_context.ClubMembershipCycles.Add(newCycle);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
ClubMembershipCycleHistoryWriter.WriteCycleStarted(_context, newCycle, reason: "فعالسازی عضویت باشگاه");
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Created ClubMembershipCycle #{CycleNumber} for UserId {UserId}, MembershipId {MembershipId}",
|
"Created ClubMembershipCycle #{CycleNumber} for UserId {UserId}, MembershipId {MembershipId}",
|
||||||
newCycle.CycleNumber,
|
newCycle.CycleNumber,
|
||||||
@@ -307,6 +323,11 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری
|
// ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری
|
||||||
|
// عضو جدید (isNewMembership=true): Pool توسط AcceptClubMembershipContract شارژ میشود
|
||||||
|
// چون کاربر قبل از امضای قرارداد IsActive=false است و قرارداد فقط یکبار امضا میشود.
|
||||||
|
// خرید مجدد (isNewMembership=false): قرارداد مجدد امضا نمیشود — Pool همینجا شارژ میشود.
|
||||||
|
if (!isNewMembership)
|
||||||
|
{
|
||||||
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
|
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
|
||||||
var weeklyPool = await _context.WeeklyCommissionPools
|
var weeklyPool = await _context.WeeklyCommissionPools
|
||||||
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
|
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
|
||||||
@@ -319,8 +340,8 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
WeekDefinitionId = currentWeekDefinitionId,
|
WeekDefinitionId = currentWeekDefinitionId,
|
||||||
PackageId = package.Id,
|
PackageId = package.Id,
|
||||||
TotalPoolAmount = activationFeeValue,
|
TotalPoolAmount = activationFeeValue,
|
||||||
TotalBalances = 0, // در CalculateWeeklyBalances محاسبه میشه
|
TotalBalances = 0,
|
||||||
ValuePerBalance = 0, // در CalculateWeeklyCommissionPool محاسبه میشه
|
ValuePerBalance = 0,
|
||||||
IsCalculated = false,
|
IsCalculated = false,
|
||||||
CalculatedAt = null
|
CalculatedAt = null
|
||||||
};
|
};
|
||||||
@@ -330,12 +351,11 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Created new WeeklyCommissionPool for WeekDefinitionId={WeekDefinitionId} with initial amount: {Amount}",
|
"Created new WeeklyCommissionPool for WeekDefinitionId={WeekDefinitionId} with initial amount: {Amount}",
|
||||||
currentWeekDefinitionId,
|
currentWeekDefinitionId,
|
||||||
giftValue
|
activationFeeValue
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// اضافه کردن به Pool موجود
|
|
||||||
weeklyPool.TotalPoolAmount += activationFeeValue;
|
weeklyPool.TotalPoolAmount += activationFeeValue;
|
||||||
_context.WeeklyCommissionPools.Update(weeklyPool);
|
_context.WeeklyCommissionPools.Update(weeklyPool);
|
||||||
|
|
||||||
@@ -348,6 +368,14 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"New member — Pool charge deferred to AcceptClubMembershipContract for UserId {UserId}",
|
||||||
|
user.Id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 9. اعمال ویژگیهای پکیج — DIFF/تفاضل (Q20)
|
// 9. اعمال ویژگیهای پکیج — DIFF/تفاضل (Q20)
|
||||||
await ApplyFeatureDiffAsync(user.Id, entity.Id, package.Id, isNewMembership, activationDate, cancellationToken);
|
await ApplyFeatureDiffAsync(user.Id, entity.Id, package.Id, isNewMembership, activationDate, cancellationToken);
|
||||||
|
|||||||
+4
-1
@@ -13,6 +13,7 @@ public class GetAllClubMembershipsQueryHandler : IRequestHandler<GetAllClubMembe
|
|||||||
{
|
{
|
||||||
var query = _context.ClubMemberships
|
var query = _context.ClubMemberships
|
||||||
.Include(x => x.User)
|
.Include(x => x.User)
|
||||||
|
.Include(x => x.LastPackage)
|
||||||
.ApplyOrder(sortBy: request.SortBy)
|
.ApplyOrder(sortBy: request.SortBy)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
@@ -35,8 +36,10 @@ public class GetAllClubMembershipsQueryHandler : IRequestHandler<GetAllClubMembe
|
|||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
UserId = x.UserId,
|
UserId = x.UserId,
|
||||||
UserName = x.User != null ? $"{x.User.FirstName} {x.User.LastName}" : string.Empty,
|
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,
|
IsActive = x.IsActive,
|
||||||
ActivatedAt = x.ActivatedAt,
|
ActivatedAt = x.LastActivationDate ?? x.ActivatedAt,
|
||||||
InitialContribution = x.InitialContribution,
|
InitialContribution = x.InitialContribution,
|
||||||
TotalEarned = x.TotalEarned,
|
TotalEarned = x.TotalEarned,
|
||||||
Created = x.Created,
|
Created = x.Created,
|
||||||
|
|||||||
+2
@@ -11,6 +11,8 @@ public class GetAllClubMembershipsResponseModel
|
|||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
public long UserId { get; set; }
|
public long UserId { get; set; }
|
||||||
public string UserName { get; set; } = string.Empty;
|
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 bool IsActive { get; set; }
|
||||||
public DateTime? ActivatedAt { get; set; }
|
public DateTime? ActivatedAt { get; set; }
|
||||||
public long InitialContribution { get; set; }
|
public long InitialContribution { get; set; }
|
||||||
|
|||||||
+10
-20
@@ -1,27 +1,17 @@
|
|||||||
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembershipHistory;
|
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembershipHistory;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Query برای دریافت تاریخچه تغییرات عضویت باشگاه
|
|
||||||
/// </summary>
|
|
||||||
public record GetClubMembershipHistoryQuery : IRequest<GetClubMembershipHistoryResponseDto>
|
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; }
|
public PaginationState? PaginationState { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// مرتبسازی بر اساس
|
|
||||||
/// </summary>
|
|
||||||
public string? SortBy { get; init; }
|
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)
|
public async Task<GetClubMembershipHistoryResponseDto> Handle(GetClubMembershipHistoryQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var query = _context.ClubMembershipHistories
|
var query = _context.ClubMembershipHistories
|
||||||
|
.Include(x => x.ClubMembership)
|
||||||
|
.ThenInclude(m => m!.User)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsQueryable();
|
.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.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
||||||
{
|
|
||||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "Created"); // پیشفرض: جدیدترین اول
|
|
||||||
|
|
||||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||||
|
|
||||||
@@ -36,6 +46,9 @@ public class GetClubMembershipHistoryQueryHandler : IRequestHandler<GetClubMembe
|
|||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
ClubMembershipId = x.ClubMembershipId,
|
ClubMembershipId = x.ClubMembershipId,
|
||||||
UserId = x.UserId,
|
UserId = x.UserId,
|
||||||
|
UserName = x.ClubMembership != null && x.ClubMembership.User != null
|
||||||
|
? $"{x.ClubMembership.User.FirstName} {x.ClubMembership.User.LastName}"
|
||||||
|
: string.Empty,
|
||||||
OldIsActive = x.OldIsActive,
|
OldIsActive = x.OldIsActive,
|
||||||
NewIsActive = x.NewIsActive,
|
NewIsActive = x.NewIsActive,
|
||||||
OldInitialContribution = x.OldInitialContribution,
|
OldInitialContribution = x.OldInitialContribution,
|
||||||
|
|||||||
+1
-26
@@ -4,31 +4,6 @@ public class GetClubMembershipHistoryQueryValidator : AbstractValidator<GetClubM
|
|||||||
{
|
{
|
||||||
public GetClubMembershipHistoryQueryValidator()
|
public GetClubMembershipHistoryQueryValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.MembershipId)
|
// Admin report: filters are all optional
|
||||||
.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 باید مقداردهی شود");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 Id { get; set; }
|
||||||
public long ClubMembershipId { get; set; }
|
public long ClubMembershipId { get; set; }
|
||||||
public long UserId { get; set; }
|
public long UserId { get; set; }
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
public bool OldIsActive { get; set; }
|
public bool OldIsActive { get; set; }
|
||||||
public bool NewIsActive { get; set; }
|
public bool NewIsActive { get; set; }
|
||||||
public long? OldInitialContribution { 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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
namespace CMSMicroservice.Application.CommissionCQ.Commands.BulkCreditPayouts;
|
||||||
|
|
||||||
|
public class BulkCreditPayoutsCommand : IRequest<BulkCreditPayoutsResult>
|
||||||
|
{
|
||||||
|
public long WeekDefinitionId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BulkCreditPayoutsResult
|
||||||
|
{
|
||||||
|
public int CreditedCount { get; set; }
|
||||||
|
public long TotalCredited { get; set; }
|
||||||
|
}
|
||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities.Commission;
|
||||||
|
using CMSMicroservice.Domain.Entities.History;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.CommissionCQ.Commands.BulkCreditPayouts;
|
||||||
|
|
||||||
|
public class BulkCreditPayoutsCommandHandler : IRequestHandler<BulkCreditPayoutsCommand, BulkCreditPayoutsResult>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
private readonly ILogger<BulkCreditPayoutsCommandHandler> _logger;
|
||||||
|
|
||||||
|
public BulkCreditPayoutsCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
ICurrentUserService currentUser,
|
||||||
|
ILogger<BulkCreditPayoutsCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BulkCreditPayoutsResult> Handle(BulkCreditPayoutsCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var pendingPayouts = await _context.UserCommissionPayouts
|
||||||
|
.Where(p => p.WeekDefinitionId == request.WeekDefinitionId
|
||||||
|
&& p.Status == CommissionPayoutStatus.Pending
|
||||||
|
&& !p.IsDeleted)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!pendingPayouts.Any())
|
||||||
|
{
|
||||||
|
return new BulkCreditPayoutsResult { CreditedCount = 0, TotalCredited = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
var performedBy = _currentUser.GetPerformedBy() ?? "System";
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
var payoutHistories = new List<CommissionPayoutHistory>();
|
||||||
|
var creditedCount = 0;
|
||||||
|
long totalCredited = 0;
|
||||||
|
|
||||||
|
foreach (var payout in pendingPayouts)
|
||||||
|
{
|
||||||
|
payout.Status = CommissionPayoutStatus.Paid;
|
||||||
|
payout.PaidAt = now;
|
||||||
|
payout.LastModified = now;
|
||||||
|
|
||||||
|
payoutHistories.Add(new CommissionPayoutHistory
|
||||||
|
{
|
||||||
|
UserCommissionPayoutId = payout.Id,
|
||||||
|
UserId = payout.UserId,
|
||||||
|
WeekDefinitionId = payout.WeekDefinitionId,
|
||||||
|
AmountBefore = payout.TotalAmount,
|
||||||
|
AmountAfter = payout.TotalAmount,
|
||||||
|
OldStatus = CommissionPayoutStatus.Pending,
|
||||||
|
NewStatus = CommissionPayoutStatus.Paid,
|
||||||
|
Action = CommissionPayoutAction.Paid,
|
||||||
|
PerformedBy = performedBy,
|
||||||
|
Reason = $"تایید پرداخت دستهجمعی کمیسیون هفته {payout.WeekDefinitionId} توسط {performedBy}"
|
||||||
|
});
|
||||||
|
|
||||||
|
creditedCount++;
|
||||||
|
totalCredited += payout.TotalAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.CommissionPayoutHistories.AddRangeAsync(payoutHistories, cancellationToken);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"BulkCreditPayouts completed: Week={WeekId}, Credited={Count}, TotalAmount={Total}, By={By}",
|
||||||
|
request.WeekDefinitionId, creditedCount, totalCredited, performedBy);
|
||||||
|
|
||||||
|
return new BulkCreditPayoutsResult
|
||||||
|
{
|
||||||
|
CreditedCount = creditedCount,
|
||||||
|
TotalCredited = totalCredited
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-5
@@ -266,12 +266,16 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
|||||||
|
|
||||||
var count = 0;
|
var count = 0;
|
||||||
|
|
||||||
// اگر فرزند در این هفته پکیج خریده (Cycle جدید ساخته شده)، 1 امتیاز
|
// ⭐ بر اساس بازه تاریخی هفته، نه IsCurrentCycle
|
||||||
// ⚠️ از ClubMembershipCycle.PackagePurchasedAt استفاده میکنیم (نه ActivatedAt که overwrite میشد)
|
// IsCurrentCycle فقط آخرین خرید را نشان میدهد — برای تاریخچهای (هفتههای گذشته)
|
||||||
var currentCycle = await _context.ClubMembershipCycles
|
// باید از PackagePurchasedAt استفاده کرد که تاریخ دقیق خرید را نگه میدارد
|
||||||
.FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsCurrentCycle, cancellationToken);
|
var hadCycleInWeek = await _context.ClubMembershipCycles
|
||||||
|
.AnyAsync(x => x.UserId == child.Id
|
||||||
|
&& x.PackagePurchasedAt >= startDate
|
||||||
|
&& x.PackagePurchasedAt <= endDate,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
if (currentCycle?.PackagePurchasedAt >= startDate && currentCycle?.PackagePurchasedAt <= endDate)
|
if (hadCycleInWeek)
|
||||||
{
|
{
|
||||||
count = 1;
|
count = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -10,6 +10,11 @@ public record CalculateWeeklyCommissionPoolCommand : IRequest<long>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public long WeekDefinitionId { get; init; }
|
public long WeekDefinitionId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه پکیج — اگر مقدار داشته باشد فقط همین پکیج محاسبه میشود.
|
||||||
|
/// </summary>
|
||||||
|
public long? PackageId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// آیا محاسبه مجدد انجام شود؟
|
/// آیا محاسبه مجدد انجام شود؟
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+29
-8
@@ -23,8 +23,17 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// بررسی وجود استخر
|
// بررسی وجود استخر
|
||||||
var existingPool = await _context.WeeklyCommissionPools
|
var existingPoolQuery = _context.WeeklyCommissionPools
|
||||||
.FirstOrDefaultAsync(x => x.WeekDefinitionId == request.WeekDefinitionId, cancellationToken);
|
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId);
|
||||||
|
|
||||||
|
if (request.PackageId.HasValue)
|
||||||
|
{
|
||||||
|
existingPoolQuery = existingPoolQuery.Where(x => x.PackageId == request.PackageId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingPool = await existingPoolQuery
|
||||||
|
.OrderBy(x => x.Id)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
if (existingPool == null)
|
if (existingPool == null)
|
||||||
{
|
{
|
||||||
@@ -40,9 +49,15 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// بررسی وجود تعادلهای هفتگی
|
// بررسی وجود تعادلهای هفتگی
|
||||||
var weeklyBalances = await _context.NetworkWeeklyBalances
|
var weeklyBalancesQuery = _context.NetworkWeeklyBalances
|
||||||
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId)
|
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId);
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
if (request.PackageId.HasValue)
|
||||||
|
{
|
||||||
|
weeklyBalancesQuery = weeklyBalancesQuery.Where(x => x.PackageId == request.PackageId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var weeklyBalances = await weeklyBalancesQuery.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
if (!weeklyBalances.Any())
|
if (!weeklyBalances.Any())
|
||||||
{
|
{
|
||||||
@@ -76,9 +91,15 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
|
|||||||
// حذف پرداختهای قبلی در صورت ForceRecalculate
|
// حذف پرداختهای قبلی در صورت ForceRecalculate
|
||||||
if (request.ForceRecalculate)
|
if (request.ForceRecalculate)
|
||||||
{
|
{
|
||||||
var oldPayouts = await _context.UserCommissionPayouts
|
var oldPayoutsQuery = _context.UserCommissionPayouts
|
||||||
.Where(p => p.WeekDefinitionId == request.WeekDefinitionId)
|
.Where(p => p.WeekDefinitionId == request.WeekDefinitionId);
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
if (request.PackageId.HasValue)
|
||||||
|
{
|
||||||
|
oldPayoutsQuery = oldPayoutsQuery.Where(p => p.PackageId == request.PackageId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldPayouts = await oldPayoutsQuery.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
if (oldPayouts.Any())
|
if (oldPayouts.Any())
|
||||||
{
|
{
|
||||||
|
|||||||
+6
-1
@@ -1,3 +1,5 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.CommissionCQ.Commands.RequestWithdrawal;
|
namespace CMSMicroservice.Application.CommissionCQ.Commands.RequestWithdrawal;
|
||||||
|
|
||||||
public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawalCommand, Unit>
|
public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawalCommand, Unit>
|
||||||
@@ -37,7 +39,10 @@ public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawal
|
|||||||
|
|
||||||
if (request.Method == WithdrawalMethod.Cash)
|
if (request.Method == WithdrawalMethod.Cash)
|
||||||
{
|
{
|
||||||
payout.IbanNumber = request.IbanNumber;
|
var normalizedIban = IbanNormalizer.TryNormalize(request.IbanNumber);
|
||||||
|
if (normalizedIban is null)
|
||||||
|
throw new InvalidOperationException("فرمت شماره شبا معتبر نیست. باید IR و ۲۴ رقم باشد.");
|
||||||
|
payout.IbanNumber = normalizedIban;
|
||||||
}
|
}
|
||||||
|
|
||||||
_context.UserCommissionPayouts.Update(payout);
|
_context.UserCommissionPayouts.Update(payout);
|
||||||
|
|||||||
+35
-61
@@ -15,75 +15,49 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
|||||||
|
|
||||||
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetUserWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetUserWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var query = _context.NetworkWeeklyBalances
|
var pageSize = request.PaginationState?.PageSize > 0 ? request.PaginationState.PageSize : PaginationDefaults.PageSize;
|
||||||
.Include(x => x.WeekDefinition)
|
var pageNumber = request.PaginationState?.PageNumber > 0 ? request.PaginationState.PageNumber : PaginationDefaults.PageNumber;
|
||||||
.Include(x => x.User)
|
|
||||||
.Include(x => x.Package)
|
|
||||||
.AsNoTracking()
|
|
||||||
.AsQueryable();
|
|
||||||
|
|
||||||
// UserId > 0 → filter by that user
|
// تشخیص جهت مرتبسازی از SortBy (پیشفرض: نزولی)
|
||||||
// UserId == 0 or null → show ALL users (admin mode)
|
var sortBy = request.SortBy ?? "-WeekDefinitionId";
|
||||||
// Customer endpoints resolve UserId from JWT before calling this handler
|
int sortDescending = sortBy.StartsWith("-") ? 1 : 0;
|
||||||
long? userId = request.UserId;
|
|
||||||
|
|
||||||
if (userId.HasValue && userId.Value > 0)
|
long? userId = request.UserId.HasValue && request.UserId.Value > 0 ? request.UserId : null;
|
||||||
{
|
long? weekDefinitionId = request.WeekDefinitionId.HasValue ? request.WeekDefinitionId : null;
|
||||||
query = query.Where(x => x.UserId == userId.Value);
|
long? packageId = request.PackageId.HasValue && request.PackageId.Value > 0 ? request.PackageId : null;
|
||||||
}
|
int onlyActive = request.OnlyActive.HasValue && request.OnlyActive.Value ? 1 : 0;
|
||||||
|
|
||||||
// فیلتر بر اساس WeekDefinitionId (روش ترجیحی)
|
var rows = await _context.Database
|
||||||
if (request.WeekDefinitionId.HasValue)
|
.SqlQuery<GetUserWeeklyBalancesResponseModel>(
|
||||||
{
|
$"""
|
||||||
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId.Value);
|
EXEC [CMS].[sp_GetUserWeeklyBalances]
|
||||||
}
|
@UserId = {userId},
|
||||||
|
@WeekDefinitionId = {weekDefinitionId},
|
||||||
|
@PackageId = {packageId},
|
||||||
if (request.OnlyActive.HasValue && request.OnlyActive.Value)
|
@OnlyActive = {onlyActive},
|
||||||
{
|
@SortDescending = {sortDescending},
|
||||||
query = query.Where(x => !x.IsExpired);
|
@PageNumber = {pageNumber},
|
||||||
}
|
@PageSize = {pageSize}
|
||||||
|
""")
|
||||||
// فیلتر بر اساس پکیج
|
|
||||||
if (request.PackageId.HasValue && request.PackageId.Value > 0)
|
|
||||||
{
|
|
||||||
query = query.Where(x => x.PackageId == request.PackageId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// مرتبسازی بر اساس WeekDefinitionId (نزولی = جدیدترین اول)
|
|
||||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "-WeekDefinitionId");
|
|
||||||
|
|
||||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
|
||||||
|
|
||||||
var models = await query
|
|
||||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
|
||||||
.Select(x => new GetUserWeeklyBalancesResponseModel
|
|
||||||
{
|
|
||||||
Id = x.Id,
|
|
||||||
UserId = x.UserId,
|
|
||||||
UserFullName = x.User != null ? $"{x.User.FirstName} {x.User.LastName}".Trim() : "",
|
|
||||||
WeekDefinitionId = x.WeekDefinitionId,
|
|
||||||
WeekDisplayName = x.WeekDefinition != null ? x.WeekDefinition.DisplayName : "",
|
|
||||||
LeftLegNewMembers = x.LeftLegNewMembers,
|
|
||||||
LeftLegCarryover = x.LeftLegCarryover,
|
|
||||||
LeftLegTotal = x.LeftLegTotal,
|
|
||||||
RightLegNewMembers = x.RightLegNewMembers,
|
|
||||||
RightLegCarryover = x.RightLegCarryover,
|
|
||||||
RightLegTotal = x.RightLegTotal,
|
|
||||||
TotalBalances = x.TotalBalances,
|
|
||||||
WeeklyPoolContribution = x.WeeklyPoolContribution,
|
|
||||||
CalculatedAt = x.CalculatedAt,
|
|
||||||
IsExpired = x.IsExpired,
|
|
||||||
Created = x.Created,
|
|
||||||
PackageId = x.PackageId,
|
|
||||||
PackageTitle = x.Package != null ? x.Package.Title : ""
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var totalCount = rows.Count > 0 ? rows[0].TotalCount : 0;
|
||||||
|
var totalPageCount = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||||
|
|
||||||
|
var meta = new MetaData
|
||||||
|
{
|
||||||
|
CurrentPage = pageNumber,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
TotalPage = totalPageCount,
|
||||||
|
HasNext = pageNumber < totalPageCount,
|
||||||
|
HasPrevious = pageNumber > 1
|
||||||
|
};
|
||||||
|
|
||||||
return new GetUserWeeklyBalancesResponseDto
|
return new GetUserWeeklyBalancesResponseDto
|
||||||
{
|
{
|
||||||
MetaData = meta,
|
MetaData = meta,
|
||||||
Models = models
|
Models = rows
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -33,4 +33,7 @@ public class GetUserWeeklyBalancesResponseModel
|
|||||||
// Package info
|
// Package info
|
||||||
public long PackageId { get; set; }
|
public long PackageId { get; set; }
|
||||||
public string PackageTitle { get; set; } = string.Empty;
|
public string PackageTitle { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// نتیجه COUNT(*) OVER() از SP — فقط در خواندن از SP پر میشود
|
||||||
|
public int TotalCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-1
@@ -41,6 +41,61 @@ public class GetWeeklyCommissionPoolQueryHandler : IRequestHandler<GetWeeklyComm
|
|||||||
})
|
})
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
return pool ?? new WeeklyCommissionPoolDto();
|
pool ??= new WeeklyCommissionPoolDto();
|
||||||
|
|
||||||
|
// ── Q29: اعتبارسنجی TotalPoolAmount در برابر فعالسازیهای واقعی ──
|
||||||
|
await ValidatePoolAmountAsync(pool, request, cancellationToken);
|
||||||
|
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TotalPoolAmount استخر را با تعداد فعالسازیهای ثبتشده (AcceptContract + ActivateClubMembership) مقایسه میکند.
|
||||||
|
/// نتیجه در فیلدهای Validation... ذخیره میشود.
|
||||||
|
/// </summary>
|
||||||
|
private async Task ValidatePoolAmountAsync(WeeklyCommissionPoolDto pool, GetWeeklyCommissionPoolQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var packageId = pool.PackageId > 0 ? pool.PackageId : request.PackageId;
|
||||||
|
if (packageId is null or 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var weekDef = _weekDefinitionRepository.GetWeekById(request.WeekDefinitionId);
|
||||||
|
if (weekDef is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var activationFee = await _context.Packages
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(p => p.Id == packageId.Value)
|
||||||
|
.Select(p => p.ActivationFee)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (activationFee <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// شمارش Cycleهایی که Pool آنها واقعاً شارژ شده:
|
||||||
|
// - عضو جدید: Pool توسط AcceptContract شارژ میشود → UserContract وجود دارد
|
||||||
|
// - خرید مجدد: Pool در ActivateClubMembership شارژ میشود → IsCurrentCycle نبوده (قبلاً active بوده)
|
||||||
|
// - نباید از IsActive استفاده شود: وضعیت تاریخی نیست (عضو ممکن است بعداً غیرفعال شده باشد)
|
||||||
|
//
|
||||||
|
// قانون: Cycle شارژ شده = یا قرارداد دارد (عضو جدید) یا IsActive قبلاً true بوده (خرید مجدد)
|
||||||
|
// سادهترین معادل: Cycle هایی که ClubMembership.ActivatedAt در بازه هفته است (زمان واقعی شارژ Pool)
|
||||||
|
var totalActivations = await _context.ClubMembershipCycles
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(c => c.PackageId == packageId.Value
|
||||||
|
&& c.PackagePurchasedAt >= weekDef.StartDate
|
||||||
|
&& c.PackagePurchasedAt < weekDef.EndDate
|
||||||
|
&& _context.UserContracts.Any(uc =>
|
||||||
|
uc.UserId == c.UserId
|
||||||
|
&& _context.Contracts.Any(con =>
|
||||||
|
con.Id == uc.ContractId
|
||||||
|
&& con.Type == ContractType.ClubMembership)))
|
||||||
|
.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var expectedAmount = (long)totalActivations * activationFee;
|
||||||
|
|
||||||
|
pool.ValidationActivationCount = totalActivations;
|
||||||
|
pool.ValidationExpectedAmount = expectedAmount;
|
||||||
|
pool.ValidationDiscrepancy = pool.TotalPoolAmount - expectedAmount;
|
||||||
|
pool.ValidationIsValid = pool.TotalPoolAmount == expectedAmount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -16,4 +16,14 @@ public class WeeklyCommissionPoolDto
|
|||||||
public DateTimeOffset Created { get; set; }
|
public DateTimeOffset Created { get; set; }
|
||||||
public long PackageId { get; set; }
|
public long PackageId { get; set; }
|
||||||
public string PackageTitle { get; set; } = string.Empty;
|
public string PackageTitle { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// ── Validation Fields (Q29) ──
|
||||||
|
/// <summary>آیا TotalPoolAmount با تعداد فعالسازیهای ثبتشده در دیتابیس مطابقت دارد؟</summary>
|
||||||
|
public bool ValidationIsValid { get; set; } = true;
|
||||||
|
/// <summary>مبلغ مورد انتظار بر اساس شمارش فعالسازیها × ActivationFee</summary>
|
||||||
|
public long ValidationExpectedAmount { get; set; }
|
||||||
|
/// <summary>اختلاف: TotalPoolAmount - ValidationExpectedAmount</summary>
|
||||||
|
public long ValidationDiscrepancy { get; set; }
|
||||||
|
/// <summary>تعداد فعالسازیهای شناساییشده برای این هفته و پکیج</summary>
|
||||||
|
public int ValidationActivationCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.Common;
|
||||||
|
|
||||||
|
public static class BlogSlugHelper
|
||||||
|
{
|
||||||
|
private const int MaxSlugLength = 200;
|
||||||
|
|
||||||
|
public static string? NormalizeOptional(string? slug)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(slug))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return slug.Trim().ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a URL slug from title (ASCII letters/digits). Persian-only titles yield empty string.
|
||||||
|
/// </summary>
|
||||||
|
public static string GenerateFromTitle(string title)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(title))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
var lastWasHyphen = false;
|
||||||
|
|
||||||
|
foreach (var c in title.Trim().ToLowerInvariant())
|
||||||
|
{
|
||||||
|
if (c is >= 'a' and <= 'z' or >= '0' and <= '9')
|
||||||
|
{
|
||||||
|
sb.Append(c);
|
||||||
|
lastWasHyphen = false;
|
||||||
|
}
|
||||||
|
else if (c is ' ' or '_' or '-')
|
||||||
|
{
|
||||||
|
if (sb.Length > 0 && !lastWasHyphen)
|
||||||
|
{
|
||||||
|
sb.Append('-');
|
||||||
|
lastWasHyphen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = sb.ToString().Trim('-');
|
||||||
|
return result.Length > MaxSlugLength ? result[..MaxSlugLength].TrimEnd('-') : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string CreateFallbackSlug() =>
|
||||||
|
$"post-{Guid.NewGuid():N}"[..Math.Min(20, MaxSlugLength)];
|
||||||
|
|
||||||
|
public static string ResolveForCreate(string? requestedSlug, string title)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeOptional(requestedSlug);
|
||||||
|
if (!string.IsNullOrEmpty(normalized))
|
||||||
|
return normalized;
|
||||||
|
|
||||||
|
var fromTitle = GenerateFromTitle(title);
|
||||||
|
return !string.IsNullOrEmpty(fromTitle) ? fromTitle : CreateFallbackSlug();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> EnsureUniquePostSlugAsync(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
string slug,
|
||||||
|
long? excludePostId = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var candidate = slug;
|
||||||
|
var suffix = 2;
|
||||||
|
|
||||||
|
while (await context.BlogPosts.AnyAsync(
|
||||||
|
p => p.Slug == candidate && !p.IsDeleted && (excludePostId == null || p.Id != excludePostId.Value),
|
||||||
|
cancellationToken))
|
||||||
|
{
|
||||||
|
var suffixStr = $"-{suffix}";
|
||||||
|
var maxBase = MaxSlugLength - suffixStr.Length;
|
||||||
|
var basePart = slug.Length > maxBase ? slug[..maxBase].TrimEnd('-') : slug;
|
||||||
|
candidate = basePart + suffixStr;
|
||||||
|
suffix++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when a concurrent payment operation is already in progress for the same scope.
|
||||||
|
/// </summary>
|
||||||
|
public class PaymentInProgressException : Exception
|
||||||
|
{
|
||||||
|
public PaymentInProgressException()
|
||||||
|
: base("یک عملیات پرداخت دیگر در حال پردازش است. لطفاً چند لحظه صبر کنید.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaymentInProgressException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaymentInProgressException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
using System.Linq.Dynamic.Core;
|
using System.Linq.Dynamic.Core;
|
||||||
using CMSMicroservice.Domain.Common;
|
using CMSMicroservice.Domain.Common;
|
||||||
|
|
||||||
@@ -8,16 +9,46 @@ public static class SortByExtensions
|
|||||||
public static IQueryable<TSource> ApplyOrder<TSource>(this IQueryable<TSource> source,
|
public static IQueryable<TSource> ApplyOrder<TSource>(this IQueryable<TSource> source,
|
||||||
string? sortBy) where TSource : BaseAuditableEntity
|
string? sortBy) where TSource : BaseAuditableEntity
|
||||||
{
|
{
|
||||||
// default sort approach
|
|
||||||
if (sortBy is null or "")
|
if (sortBy is null or "")
|
||||||
{
|
{
|
||||||
source = source.OrderByDescending(p => p.Created);
|
return source.OrderByDescending(p => p.Created);
|
||||||
return source;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort using dynamic linq
|
return source.OrderBy(NormalizeSortBy(sortBy));
|
||||||
source = source.OrderBy(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;
|
||||||
|
}
|
||||||
@@ -47,6 +47,17 @@ public interface IPaymentGatewayService
|
|||||||
"درگاه پرداخت باید متد VerifyPaymentAsync با مبلغ را پیادهسازی کند");
|
"درگاه پرداخت باید متد VerifyPaymentAsync با مبلغ را پیادهسازی کند");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فهرست Authorityهایی که کاربر پرداخت کرده ولی سیستم هنوز Verify نکرده
|
||||||
|
/// این متد برای Reconciliation در background job استفاده میشود.
|
||||||
|
/// پیادهسازی پیشفرض لیست خالی برمیگرداند (درگاههایی که این API را ندارند).
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<string>> GetUnverifiedAuthoritiesAsync(
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IReadOnlyList<string>>(Array.Empty<string>());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// واریز مبلغ به حساب کاربر (برداشت از کیف پول)
|
/// واریز مبلغ به حساب کاربر (برداشت از کیف پول)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-scope in-process mutex for payment flows (initiate / verify).
|
||||||
|
/// Prevents the same user from running duplicate gateway operations concurrently.
|
||||||
|
/// </summary>
|
||||||
|
public interface IUserPaymentLock
|
||||||
|
{
|
||||||
|
Task<T> ExecuteAsync<T>(
|
||||||
|
string scope,
|
||||||
|
PaymentLockStrategy strategy,
|
||||||
|
Func<CancellationToken, Task<T>> action,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task ExecuteAsync(
|
||||||
|
string scope,
|
||||||
|
PaymentLockStrategy strategy,
|
||||||
|
Func<CancellationToken, Task> action,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How to behave when the payment lock is already held.
|
||||||
|
/// </summary>
|
||||||
|
public enum PaymentLockStrategy
|
||||||
|
{
|
||||||
|
/// <summary>Wait up to the configured timeout (callback / verify paths).</summary>
|
||||||
|
WaitForRelease,
|
||||||
|
|
||||||
|
/// <summary>Reject immediately (initiate / double-click paths).</summary>
|
||||||
|
FailFast
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Canonical scope keys for <see cref="Interfaces.IUserPaymentLock"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class PaymentLockScopes
|
||||||
|
{
|
||||||
|
/// <summary>One active payment initiation per user (package, wallet charge, order, IPG deposit).</summary>
|
||||||
|
public static string Initiate(long userId) => $"payment:initiate:user:{userId}";
|
||||||
|
|
||||||
|
/// <summary>One active verify per user + authority/order (duplicate callback protection).</summary>
|
||||||
|
public static string Verify(long userId, string resourceKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(resourceKey))
|
||||||
|
throw new ArgumentException("Payment verify resource key is required.", nameof(resourceKey));
|
||||||
|
|
||||||
|
return $"payment:verify:user:{userId}:{resourceKey.Trim()}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-2
@@ -1,3 +1,5 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Application.Common.Services;
|
using CMSMicroservice.Application.Common.Services;
|
||||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||||
@@ -17,22 +19,32 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
|
|||||||
private readonly IPaymentGatewayService _paymentGateway;
|
private readonly IPaymentGatewayService _paymentGateway;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<PlaceOrderCommandHandler> _logger;
|
private readonly ILogger<PlaceOrderCommandHandler> _logger;
|
||||||
|
private readonly IUserPaymentLock _paymentLock;
|
||||||
|
|
||||||
public PlaceOrderCommandHandler(
|
public PlaceOrderCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IInventoryService inventoryService,
|
IInventoryService inventoryService,
|
||||||
IPaymentGatewayService paymentGateway,
|
IPaymentGatewayService paymentGateway,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<PlaceOrderCommandHandler> logger)
|
ILogger<PlaceOrderCommandHandler> logger,
|
||||||
|
IUserPaymentLock paymentLock)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_inventoryService = inventoryService;
|
_inventoryService = inventoryService;
|
||||||
_paymentGateway = paymentGateway;
|
_paymentGateway = paymentGateway;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_paymentLock = paymentLock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PlaceOrderResponseDto> Handle(PlaceOrderCommand request, CancellationToken cancellationToken)
|
public Task<PlaceOrderResponseDto> Handle(PlaceOrderCommand request, CancellationToken cancellationToken) =>
|
||||||
|
_paymentLock.ExecuteAsync(
|
||||||
|
PaymentLockScopes.Initiate(request.UserId),
|
||||||
|
PaymentLockStrategy.FailFast,
|
||||||
|
ct => HandleCore(request, ct),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
private async Task<PlaceOrderResponseDto> HandleCore(PlaceOrderCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Get user wallet
|
// Get user wallet
|
||||||
var userWallet = await _context.UserWallets
|
var userWallet = await _context.UserWallets
|
||||||
|
|||||||
+5
@@ -35,6 +35,11 @@ public class UpdateOrderStatusCommandHandler : IRequestHandler<UpdateOrderStatus
|
|||||||
order.TrackingCode = request.TrackingCode;
|
order.TrackingCode = request.TrackingCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.AdminNotes))
|
||||||
|
{
|
||||||
|
order.DeliveryDescription = request.AdminNotes;
|
||||||
|
}
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
return new UpdateOrderStatusResponseDto
|
return new UpdateOrderStatusResponseDto
|
||||||
|
|||||||
+36
-12
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Application.Common.Models;
|
using CMSMicroservice.Application.Common.Models;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -80,16 +81,40 @@ public class GetAllDiscountOrdersQueryHandler : IRequestHandler<GetAllDiscountOr
|
|||||||
// Apply pagination
|
// Apply pagination
|
||||||
var pagination = request.PaginationQuery ?? new PaginationState { PageNumber = 1, PageSize = 20 };
|
var pagination = request.PaginationQuery ?? new PaginationState { PageNumber = 1, PageSize = 20 };
|
||||||
|
|
||||||
var orders = await query
|
var rows = await query
|
||||||
.OrderByDescending(o => o.Created)
|
.OrderByDescending(o => o.Created)
|
||||||
.Skip((pagination.PageNumber - 1) * pagination.PageSize)
|
.Skip((pagination.PageNumber - 1) * pagination.PageSize)
|
||||||
.Take(pagination.PageSize)
|
.Take(pagination.PageSize)
|
||||||
.Select(o => new AdminOrderDto
|
.Select(o => new
|
||||||
|
{
|
||||||
|
o.Id,
|
||||||
|
o.UserId,
|
||||||
|
FirstName = o.User.FirstName,
|
||||||
|
LastName = o.User.LastName,
|
||||||
|
Mobile = o.User.Mobile,
|
||||||
|
o.TotalAmount,
|
||||||
|
o.DiscountBalanceUsed,
|
||||||
|
o.GatewayAmountPaid,
|
||||||
|
o.VatAmount,
|
||||||
|
o.PaymentStatus,
|
||||||
|
o.PaymentDate,
|
||||||
|
o.DeliveryStatus,
|
||||||
|
ShippingAddress = o.UserAddress.Address,
|
||||||
|
ReceiverName = o.UserAddress.Title,
|
||||||
|
o.TrackingCode,
|
||||||
|
AdminNote = o.DeliveryDescription,
|
||||||
|
o.Created,
|
||||||
|
o.LastModified,
|
||||||
|
ItemsCount = o.OrderDetails.Count
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var orders = rows.Select(o => new AdminOrderDto
|
||||||
{
|
{
|
||||||
Id = o.Id,
|
Id = o.Id,
|
||||||
UserId = o.UserId,
|
UserId = o.UserId,
|
||||||
UserFullName = (o.User.FirstName ?? "") + " " + (o.User.LastName ?? ""),
|
UserFullName = UserDisplayName.From(o.FirstName, o.LastName, o.Mobile, o.UserId),
|
||||||
UserMobile = o.User.Mobile,
|
UserMobile = o.Mobile,
|
||||||
TotalAmount = o.TotalAmount,
|
TotalAmount = o.TotalAmount,
|
||||||
DiscountBalanceUsed = o.DiscountBalanceUsed,
|
DiscountBalanceUsed = o.DiscountBalanceUsed,
|
||||||
GatewayAmountPaid = o.GatewayAmountPaid,
|
GatewayAmountPaid = o.GatewayAmountPaid,
|
||||||
@@ -97,17 +122,16 @@ public class GetAllDiscountOrdersQueryHandler : IRequestHandler<GetAllDiscountOr
|
|||||||
PaymentStatus = o.PaymentStatus,
|
PaymentStatus = o.PaymentStatus,
|
||||||
PaymentDate = o.PaymentDate,
|
PaymentDate = o.PaymentDate,
|
||||||
DeliveryStatus = o.DeliveryStatus,
|
DeliveryStatus = o.DeliveryStatus,
|
||||||
DeliveryDate = null, // TODO: Add DeliveryDate to DiscountOrder if needed
|
DeliveryDate = null,
|
||||||
ShippingAddress = o.UserAddress.Address,
|
ShippingAddress = o.ShippingAddress,
|
||||||
ReceiverName = o.UserAddress.Title,
|
ReceiverName = o.ReceiverName,
|
||||||
ReceiverMobile = o.User.Mobile,
|
ReceiverMobile = o.Mobile,
|
||||||
TrackingCode = o.TrackingCode,
|
TrackingCode = o.TrackingCode,
|
||||||
AdminNote = o.DeliveryDescription,
|
AdminNote = o.AdminNote,
|
||||||
Created = o.Created,
|
Created = o.Created,
|
||||||
LastModified = o.LastModified,
|
LastModified = o.LastModified,
|
||||||
ItemsCount = o.OrderDetails.Count
|
ItemsCount = o.ItemsCount
|
||||||
})
|
}).ToList();
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new GetAllDiscountOrdersResponseDto
|
return new GetAllDiscountOrdersResponseDto
|
||||||
{
|
{
|
||||||
|
|||||||
+5
@@ -8,9 +8,14 @@ public class GetDiscountProductsQuery : IRequest<GetDiscountProductsResponseDto>
|
|||||||
public PaginationState? PaginationQuery { get; set; }
|
public PaginationState? PaginationQuery { get; set; }
|
||||||
public long? CategoryId { get; set; }
|
public long? CategoryId { get; set; }
|
||||||
public string? SearchTerm { get; set; }
|
public string? SearchTerm { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// true = فقط موجود (RemainingCount > 0)، false = فقط ناموجود
|
||||||
|
/// </summary>
|
||||||
|
public bool? InStock { get; set; }
|
||||||
public bool? IsActive { get; set; }
|
public bool? IsActive { get; set; }
|
||||||
public int? MinPrice { get; set; }
|
public int? MinPrice { get; set; }
|
||||||
public int? MaxPrice { get; set; }
|
public int? MaxPrice { get; set; }
|
||||||
|
public string? SortBy { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GetDiscountProductsResponseDto
|
public class GetDiscountProductsResponseDto
|
||||||
|
|||||||
+9
-2
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Extensions;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Application.Common.Models;
|
using CMSMicroservice.Application.Common.Models;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -41,6 +42,11 @@ public class GetDiscountProductsQueryHandler : IRequestHandler<GetDiscountProduc
|
|||||||
query = query.Where(p => p.IsActive == request.IsActive.Value);
|
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)
|
if (request.MinPrice.HasValue)
|
||||||
{
|
{
|
||||||
query = query.Where(p => p.Price >= request.MinPrice.Value);
|
query = query.Where(p => p.Price >= request.MinPrice.Value);
|
||||||
@@ -51,13 +57,14 @@ public class GetDiscountProductsQueryHandler : IRequestHandler<GetDiscountProduc
|
|||||||
query = query.Where(p => p.Price <= request.MaxPrice.Value);
|
query = query.Where(p => p.Price <= request.MaxPrice.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// موجودیدارها اول، سپس سورت درخواستی
|
||||||
|
query = query.ApplyOrderWithInStockPriority(request.SortBy, p => p.RemainingCount > 0);
|
||||||
|
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
// Apply pagination
|
|
||||||
var pagination = request.PaginationQuery ?? new PaginationState { PageNumber = 1, PageSize = 10 };
|
var pagination = request.PaginationQuery ?? new PaginationState { PageNumber = 1, PageSize = 10 };
|
||||||
|
|
||||||
var products = await query
|
var products = await query
|
||||||
.OrderByDescending(p => p.Created)
|
|
||||||
.Skip((pagination.PageNumber - 1) * pagination.PageSize)
|
.Skip((pagination.PageNumber - 1) * pagination.PageSize)
|
||||||
.Take(pagination.PageSize)
|
.Take(pagination.PageSize)
|
||||||
.Select(p => new DiscountProductDto
|
.Select(p => new DiscountProductDto
|
||||||
|
|||||||
+3
-1
@@ -6,7 +6,7 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetOrderById;
|
|||||||
public class GetOrderByIdQuery : IRequest<OrderDetailDto?>
|
public class GetOrderByIdQuery : IRequest<OrderDetailDto?>
|
||||||
{
|
{
|
||||||
public long OrderId { get; set; }
|
public long OrderId { get; set; }
|
||||||
public long UserId { get; set; }
|
public long? UserId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OrderDetailDto
|
public class OrderDetailDto
|
||||||
@@ -32,6 +32,8 @@ public class UserAddressDto
|
|||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string Address { get; set; }
|
public string Address { get; set; }
|
||||||
public string PostalCode { get; set; }
|
public string PostalCode { get; set; }
|
||||||
|
/// <summary>شماره تماس سفارشدهنده (موبایل کاربر) برای برچسب پست</summary>
|
||||||
|
public string? Phone { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OrderItemDto
|
public class OrderItemDto
|
||||||
|
|||||||
+12
-3
@@ -15,9 +15,17 @@ public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, Order
|
|||||||
|
|
||||||
public async Task<OrderDetailDto?> Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
|
public async Task<OrderDetailDto?> Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var order = await _context.DiscountOrders
|
var query = _context.DiscountOrders
|
||||||
.Where(o => o.Id == request.OrderId && o.UserId == request.UserId)
|
.Where(o => o.Id == request.OrderId);
|
||||||
|
|
||||||
|
if (request.UserId.HasValue && request.UserId.Value > 0)
|
||||||
|
{
|
||||||
|
query = query.Where(o => o.UserId == request.UserId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var order = await query
|
||||||
.Include(o => o.UserAddress)
|
.Include(o => o.UserAddress)
|
||||||
|
.Include(o => o.User)
|
||||||
.Include(o => o.OrderDetails)
|
.Include(o => o.OrderDetails)
|
||||||
.ThenInclude(od => od.Product)
|
.ThenInclude(od => od.Product)
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
@@ -43,7 +51,8 @@ public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, Order
|
|||||||
{
|
{
|
||||||
Title = order.UserAddress.Title,
|
Title = order.UserAddress.Title,
|
||||||
Address = order.UserAddress.Address,
|
Address = order.UserAddress.Address,
|
||||||
PostalCode = order.UserAddress.PostalCode
|
PostalCode = order.UserAddress.PostalCode,
|
||||||
|
Phone = order.User?.Mobile
|
||||||
},
|
},
|
||||||
Items = order.OrderDetails.Select(od => new OrderItemDto
|
Items = order.OrderDetails.Select(od => new OrderItemDto
|
||||||
{
|
{
|
||||||
|
|||||||
+39
-2
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
|
||||||
using CMSMicroservice.Application.Common.Exceptions;
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Common;
|
using CMSMicroservice.Domain.Common;
|
||||||
@@ -14,15 +15,18 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly ICurrentUserService _currentUser;
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
private readonly ISender _sender;
|
||||||
private readonly ILogger<CreateManualPaymentCommandHandler> _logger;
|
private readonly ILogger<CreateManualPaymentCommandHandler> _logger;
|
||||||
|
|
||||||
public CreateManualPaymentCommandHandler(
|
public CreateManualPaymentCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
ICurrentUserService currentUser,
|
ICurrentUserService currentUser,
|
||||||
|
ISender sender,
|
||||||
ILogger<CreateManualPaymentCommandHandler> logger)
|
ILogger<CreateManualPaymentCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_currentUser = currentUser;
|
_currentUser = currentUser;
|
||||||
|
_sender = sender;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,12 +150,45 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
|
|||||||
|
|
||||||
await _context.UserWalletHistories.AddAsync(walletLog, cancellationToken);
|
await _context.UserWalletHistories.AddAsync(walletLog, cancellationToken);
|
||||||
|
|
||||||
// 9. تنظیم روش خرید پکیج
|
// 9. تنظیم روش خرید پکیج + ثبت ledger خرید دستی
|
||||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
user.PackagePurchaseMethod = PackagePurchaseMethod.Manual;
|
||||||
|
|
||||||
|
_context.UserPackagePurchases.Add(new UserPackagePurchase
|
||||||
|
{
|
||||||
|
UserId = request.UserId,
|
||||||
|
PackageId = package.Id,
|
||||||
|
PurchaseMethod = PackagePurchaseMethod.Manual,
|
||||||
|
PurchasedAt = DateTime.Now,
|
||||||
|
Amount = balanceAmount,
|
||||||
|
TransactionId = transaction.Id
|
||||||
|
});
|
||||||
|
|
||||||
// 10. ذخیره همه تغییرات
|
// 10. ذخیره همه تغییرات
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 11. فعالسازی باشگاه مشتریان — ساخت Cycle و آمادهسازی برای AcceptContract
|
||||||
|
// این مرحله تضمین میکند که:
|
||||||
|
// الف) ClubMembershipCycle ساخته شود تا والدین امتیاز «عضو جدید» بگیرند
|
||||||
|
// ب) Pool توسط AcceptContract (که کاربر در FO امضا میکند) شارژ شود
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _sender.Send(new ActivateClubMembershipCommand
|
||||||
|
{
|
||||||
|
UserId = request.UserId,
|
||||||
|
ForceActivation = true,
|
||||||
|
PackageId = package.Id
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception activateEx)
|
||||||
|
{
|
||||||
|
// عضویت باشگاه اختیاری است — خطا نباید پرداخت دستی را برگرداند
|
||||||
|
_logger.LogWarning(
|
||||||
|
activateEx,
|
||||||
|
"Club membership activation failed after manual payment for UserId {UserId} — payment recorded successfully",
|
||||||
|
request.UserId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Manual membership payment created successfully. " +
|
"Manual membership payment created successfully. " +
|
||||||
"ManualPaymentId: {Id}, UserId: {UserId}, TransactionId: {TransactionId}, " +
|
"ManualPaymentId: {Id}, UserId: {UserId}, TransactionId: {TransactionId}, " +
|
||||||
|
|||||||
+1
-1
@@ -118,7 +118,7 @@ public class ProcessManualMembershipPaymentCommandHandler : IRequestHandler<Proc
|
|||||||
};
|
};
|
||||||
await _context.UserWalletHistories.AddAsync(balanceLog, cancellationToken);
|
await _context.UserWalletHistories.AddAsync(balanceLog, cancellationToken);
|
||||||
|
|
||||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
user.PackagePurchaseMethod = PackagePurchaseMethod.Manual;
|
||||||
// 10. بهروزرسانی ManualPayment با TransactionId
|
// 10. بهروزرسانی ManualPayment با TransactionId
|
||||||
manualPayment.TransactionId = transaction.Id;
|
manualPayment.TransactionId = transaction.Id;
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|||||||
+35
-2
@@ -70,7 +70,7 @@ public class GetAllManualPaymentsQueryHandler
|
|||||||
// Pagination
|
// Pagination
|
||||||
var skip = (request.PageNumber - 1) * request.PageSize;
|
var skip = (request.PageNumber - 1) * request.PageSize;
|
||||||
|
|
||||||
// دریافت PackageName از آخرین خرید پکیج کاربر
|
// دریافت PackageName پیشفرض از آخرین خرید پکیج کاربر (fallback برای دادههای قدیمی)
|
||||||
var userPackageNames = await _context.UserPackagePurchases
|
var userPackageNames = await _context.UserPackagePurchases
|
||||||
.Include(p => p.Package)
|
.Include(p => p.Package)
|
||||||
.GroupBy(p => p.UserId)
|
.GroupBy(p => p.UserId)
|
||||||
@@ -104,12 +104,45 @@ public class GetAllManualPaymentsQueryHandler
|
|||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
// PackageName دقیق هر پرداخت دستی از UserWalletHistories بر اساس TransactionId همان پرداخت
|
||||||
|
var transactionIds = data
|
||||||
|
.Where(x => x.TransactionId.HasValue)
|
||||||
|
.Select(x => x.TransactionId!.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var packageNamesByTransaction = new Dictionary<long, string>();
|
||||||
|
if (transactionIds.Count > 0)
|
||||||
|
{
|
||||||
|
packageNamesByTransaction = await _context.UserWalletHistories
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(h => h.Package)
|
||||||
|
.Where(h => h.RefrenceId.HasValue
|
||||||
|
&& transactionIds.Contains(h.RefrenceId.Value)
|
||||||
|
&& h.PackageId.HasValue)
|
||||||
|
.GroupBy(h => h.RefrenceId!.Value)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
TransactionId = g.Key,
|
||||||
|
PackageName = g.OrderByDescending(x => x.Created)
|
||||||
|
.Select(x => x.Package != null ? x.Package.Title : string.Empty)
|
||||||
|
.FirstOrDefault()
|
||||||
|
})
|
||||||
|
.ToDictionaryAsync(x => x.TransactionId, x => x.PackageName ?? string.Empty, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
// تکمیل PackageName
|
// تکمیل PackageName
|
||||||
foreach (var item in data)
|
foreach (var item in data)
|
||||||
{
|
{
|
||||||
if (userPackageNames.TryGetValue(item.UserId, out var pkgName))
|
if (item.TransactionId.HasValue && packageNamesByTransaction.TryGetValue(item.TransactionId.Value, out var txPkgName))
|
||||||
|
{
|
||||||
|
item.PackageName = txPkgName;
|
||||||
|
}
|
||||||
|
else if (userPackageNames.TryGetValue(item.UserId, out var pkgName))
|
||||||
|
{
|
||||||
item.PackageName = pkgName;
|
item.PackageName = pkgName;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var metaData = new MetaData
|
var metaData = new MetaData
|
||||||
{
|
{
|
||||||
|
|||||||
+8
-2
@@ -118,7 +118,10 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
|
|||||||
ActivationWeekDefinitionId = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDefinitionId")) ? null : reader.GetInt64(reader.GetOrdinal("ActivationWeekDefinitionId")),
|
ActivationWeekDefinitionId = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDefinitionId")) ? null : reader.GetInt64(reader.GetOrdinal("ActivationWeekDefinitionId")),
|
||||||
ActivationWeekDisplayName = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDisplayName")) ? null : reader.GetString(reader.GetOrdinal("ActivationWeekDisplayName")),
|
ActivationWeekDisplayName = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDisplayName")) ? null : reader.GetString(reader.GetOrdinal("ActivationWeekDisplayName")),
|
||||||
IsActivatedInTargetWeek = Convert.ToBoolean(reader.GetValue(reader.GetOrdinal("IsActivatedInTargetWeek"))),
|
IsActivatedInTargetWeek = Convert.ToBoolean(reader.GetValue(reader.GetOrdinal("IsActivatedInTargetWeek"))),
|
||||||
UserCreated = new DateTimeOffset(reader.GetDateTime(reader.GetOrdinal("UserCreated")))
|
UserCreated = new DateTimeOffset(reader.GetDateTime(reader.GetOrdinal("UserCreated"))),
|
||||||
|
IsNewActivation = reader.IsDBNull(reader.GetOrdinal("IsNewActivation")) ? null : (bool?)(reader.GetInt32(reader.GetOrdinal("IsNewActivation")) == 1),
|
||||||
|
PackageName = reader.IsDBNull(reader.GetOrdinal("PackageName")) ? null : reader.GetString(reader.GetOrdinal("PackageName")),
|
||||||
|
PackageId = reader.IsDBNull(reader.GetOrdinal("PackageId")) ? null : reader.GetInt64(reader.GetOrdinal("PackageId"))
|
||||||
};
|
};
|
||||||
|
|
||||||
results.Add(node);
|
results.Add(node);
|
||||||
@@ -160,7 +163,10 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
|
|||||||
ActivationWeekDefinitionId = flatNode.ActivationWeekDefinitionId,
|
ActivationWeekDefinitionId = flatNode.ActivationWeekDefinitionId,
|
||||||
ActivationWeekDisplayName = flatNode.ActivationWeekDisplayName,
|
ActivationWeekDisplayName = flatNode.ActivationWeekDisplayName,
|
||||||
IsActivatedInTargetWeek = flatNode.IsActivatedInTargetWeek,
|
IsActivatedInTargetWeek = flatNode.IsActivatedInTargetWeek,
|
||||||
UserCreated = flatNode.UserCreated
|
UserCreated = flatNode.UserCreated,
|
||||||
|
IsNewActivation = flatNode.IsNewActivation,
|
||||||
|
PackageName = flatNode.PackageName,
|
||||||
|
PackageId = flatNode.PackageId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
@@ -48,6 +48,18 @@ public class NetworkTreeDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset UserCreated { get; set; }
|
public DateTimeOffset UserCreated { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیا فعالسازی در هفته هدف اولین بار بوده؟ null = بدون Cycle
|
||||||
|
/// </summary>
|
||||||
|
public bool? IsNewActivation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نام پکیج موثر (هفته هدف یا پکیج فعلی)
|
||||||
|
/// </summary>
|
||||||
|
public string? PackageName { get; set; }
|
||||||
|
|
||||||
|
public long? PackageId { get; set; }
|
||||||
|
|
||||||
public NetworkTreeDto? LeftChild { get; set; }
|
public NetworkTreeDto? LeftChild { get; set; }
|
||||||
public NetworkTreeDto? RightChild { get; set; }
|
public NetworkTreeDto? RightChild { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -21,6 +21,19 @@ public class NetworkTreeNodeDto
|
|||||||
public bool IsActivatedInTargetWeek { get; set; }
|
public bool IsActivatedInTargetWeek { get; set; }
|
||||||
public DateTimeOffset UserCreated { get; set; }
|
public DateTimeOffset UserCreated { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیا فعالسازی در هفته هدف اولین بار بوده (true) یا خرید مجدد (false)؟
|
||||||
|
/// null یعنی Cycleای ندارد
|
||||||
|
/// </summary>
|
||||||
|
public bool? IsNewActivation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نام پکیج موثر (هفته هدف یا پکیج فعلی)
|
||||||
|
/// </summary>
|
||||||
|
public string? PackageName { get; set; }
|
||||||
|
|
||||||
|
public long? PackageId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// نام کامل کاربر
|
/// نام کامل کاربر
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+24
@@ -10,15 +10,18 @@ public class CancelOrderByAdminCommandHandler : IRequestHandler<CancelOrderByAdm
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly ICurrentUserService _currentUser;
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
private readonly IInventoryService _inventoryService;
|
||||||
private readonly ILogger<CancelOrderByAdminCommandHandler> _logger;
|
private readonly ILogger<CancelOrderByAdminCommandHandler> _logger;
|
||||||
|
|
||||||
public CancelOrderByAdminCommandHandler(
|
public CancelOrderByAdminCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
ICurrentUserService currentUser,
|
ICurrentUserService currentUser,
|
||||||
|
IInventoryService inventoryService,
|
||||||
ILogger<CancelOrderByAdminCommandHandler> logger)
|
ILogger<CancelOrderByAdminCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_currentUser = currentUser;
|
_currentUser = currentUser;
|
||||||
|
_inventoryService = inventoryService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +36,7 @@ public class CancelOrderByAdminCommandHandler : IRequestHandler<CancelOrderByAdm
|
|||||||
var order = await _context.UserOrders
|
var order = await _context.UserOrders
|
||||||
.Include(x => x.User)
|
.Include(x => x.User)
|
||||||
.ThenInclude(x => x.UserWallets)
|
.ThenInclude(x => x.UserWallets)
|
||||||
|
.Include(x => x.FactorDetails)
|
||||||
.FirstOrDefaultAsync(x => x.Id == request.OrderId && !x.IsDeleted, cancellationToken);
|
.FirstOrDefaultAsync(x => x.Id == request.OrderId && !x.IsDeleted, cancellationToken);
|
||||||
|
|
||||||
if (order == null)
|
if (order == null)
|
||||||
@@ -87,6 +91,26 @@ public class CancelOrderByAdminCommandHandler : IRequestHandler<CancelOrderByAdm
|
|||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Restore inventory for each item (only if order was not yet delivered)
|
||||||
|
if (order.FactorDetails != null && order.FactorDetails.Any())
|
||||||
|
{
|
||||||
|
foreach (var item in order.FactorDetails)
|
||||||
|
{
|
||||||
|
var restored = await _inventoryService.ProcessReturnAsync(
|
||||||
|
item.ProductId, ProductType.RegularProduct,
|
||||||
|
item.Count, order.Id,
|
||||||
|
note: $"برگشت موجودی — لغو سفارش #{order.Id} توسط Admin",
|
||||||
|
ct: cancellationToken);
|
||||||
|
|
||||||
|
if (!restored)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Failed to restore inventory for ProductId={ProductId} on cancel OrderId={OrderId}",
|
||||||
|
item.ProductId, order.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Order cancelled by admin. OrderId: {OrderId}, Reason: {Reason}, Refunded: {Refunded}, Admin: {AdminId}",
|
"Order cancelled by admin. OrderId: {OrderId}, Reason: {Reason}, Refunded: {Refunded}, Admin: {AdminId}",
|
||||||
order.Id,
|
order.Id,
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared verify + wallet credit path for package IPG payments (callback UI and reconciliation job).
|
||||||
|
/// </summary>
|
||||||
|
public class VerifyUserPackagePurchasePaymentCommand : IRequest<VerifyUserPackagePurchasePaymentResult>
|
||||||
|
{
|
||||||
|
public long PurchaseId { get; set; }
|
||||||
|
public string Authority { get; set; } = string.Empty;
|
||||||
|
public string Status { get; set; } = "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VerifyUserPackagePurchasePaymentResult
|
||||||
|
{
|
||||||
|
public bool Success { get; init; }
|
||||||
|
public bool AlreadyPaid { get; init; }
|
||||||
|
public string Message { get; init; } = string.Empty;
|
||||||
|
public long TransactionId { get; init; }
|
||||||
|
public string ReferenceCode { get; init; } = string.Empty;
|
||||||
|
public long PackageId { get; init; }
|
||||||
|
public string PackageName { get; init; } = string.Empty;
|
||||||
|
public long AmountPaid { get; init; }
|
||||||
|
public DateTime PurchasedAt { get; init; }
|
||||||
|
}
|
||||||
+303
@@ -0,0 +1,303 @@
|
|||||||
|
using System.Data;
|
||||||
|
using CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
|
||||||
|
using CMSMicroservice.Application.Common;
|
||||||
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using CMSMicroservice.Domain.Entities.Payment;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
|
||||||
|
|
||||||
|
public class VerifyUserPackagePurchasePaymentCommandHandler
|
||||||
|
: IRequestHandler<VerifyUserPackagePurchasePaymentCommand, VerifyUserPackagePurchasePaymentResult>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IPaymentGatewayService _paymentGateway;
|
||||||
|
private readonly ISender _sender;
|
||||||
|
private readonly IUserPaymentLock _paymentLock;
|
||||||
|
private readonly ILogger<VerifyUserPackagePurchasePaymentCommandHandler> _logger;
|
||||||
|
|
||||||
|
public VerifyUserPackagePurchasePaymentCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IPaymentGatewayService paymentGateway,
|
||||||
|
ISender sender,
|
||||||
|
IUserPaymentLock paymentLock,
|
||||||
|
ILogger<VerifyUserPackagePurchasePaymentCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_paymentGateway = paymentGateway;
|
||||||
|
_sender = sender;
|
||||||
|
_paymentLock = paymentLock;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<VerifyUserPackagePurchasePaymentResult> Handle(
|
||||||
|
VerifyUserPackagePurchasePaymentCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var purchaseUserId = await _context.UserPackagePurchases
|
||||||
|
.Where(p => p.Id == request.PurchaseId && !p.IsDeleted)
|
||||||
|
.Select(p => (long?)p.UserId)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (purchaseUserId is null or 0)
|
||||||
|
throw new NotFoundException("سفارش یافت نشد");
|
||||||
|
|
||||||
|
var verifyKey = !string.IsNullOrEmpty(request.Authority)
|
||||||
|
? request.Authority
|
||||||
|
: request.PurchaseId.ToString();
|
||||||
|
|
||||||
|
return await _paymentLock.ExecuteAsync(
|
||||||
|
PaymentLockScopes.Verify(purchaseUserId.Value, verifyKey),
|
||||||
|
PaymentLockStrategy.WaitForRelease,
|
||||||
|
ct => HandleCoreAsync(request, ct),
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<VerifyUserPackagePurchasePaymentResult> HandleCoreAsync(
|
||||||
|
VerifyUserPackagePurchasePaymentCommand request,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var purchase = await _context.UserPackagePurchases
|
||||||
|
.Include(p => p.Package)
|
||||||
|
.Where(p => p.Id == request.PurchaseId && !p.IsDeleted)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
|
||||||
|
if (purchase == null)
|
||||||
|
throw new NotFoundException("سفارش یافت نشد");
|
||||||
|
|
||||||
|
var transaction = purchase.TransactionId.HasValue
|
||||||
|
? await _context.Transactions
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == purchase.TransactionId.Value, ct)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var paymentTx = !string.IsNullOrEmpty(request.Authority)
|
||||||
|
? await _context.PaymentTransactions
|
||||||
|
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, ct)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (IsPaymentAlreadyCompleted(transaction, paymentTx))
|
||||||
|
{
|
||||||
|
return BuildResult(purchase, transaction, paymentTx, alreadyPaid: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (transaction != null && transaction.PaymentStatus != PaymentStatus.Success)
|
||||||
|
{
|
||||||
|
transaction.PaymentStatus = PaymentStatus.Reject;
|
||||||
|
await _context.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VerifyUserPackagePurchasePaymentResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "پرداخت توسط کاربر لغو شد",
|
||||||
|
PackageId = purchase.PackageId,
|
||||||
|
PackageName = purchase.Package?.Title ?? string.Empty,
|
||||||
|
AmountPaid = purchase.Amount,
|
||||||
|
PurchasedAt = purchase.PurchasedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
|
||||||
|
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
|
||||||
|
request.Authority, request.Status, (decimal)amountInToman, ct);
|
||||||
|
|
||||||
|
if (!verifyResult.IsSuccess)
|
||||||
|
{
|
||||||
|
if (paymentTx != null)
|
||||||
|
{
|
||||||
|
paymentTx.PaymentStatus = false;
|
||||||
|
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
|
||||||
|
paymentTx.VerificationStatusMessage = verifyResult.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transaction != null)
|
||||||
|
transaction.PaymentStatus = PaymentStatus.Reject;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
return new VerifyUserPackagePurchasePaymentResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = verifyResult.Message ?? "خرید پکیج ناموفق بود",
|
||||||
|
TransactionId = transaction?.Id ?? 0,
|
||||||
|
PackageId = purchase.PackageId,
|
||||||
|
PackageName = purchase.Package?.Title ?? string.Empty,
|
||||||
|
AmountPaid = purchase.Amount,
|
||||||
|
PurchasedAt = purchase.PurchasedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var dbTx = await _context.Database.BeginTransactionAsync(IsolationLevel.Serializable, ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (purchase.TransactionId.HasValue)
|
||||||
|
{
|
||||||
|
transaction = await _context.Transactions
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == purchase.TransactionId.Value, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.Authority))
|
||||||
|
{
|
||||||
|
paymentTx = await _context.PaymentTransactions
|
||||||
|
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsPaymentAlreadyCompleted(transaction, paymentTx))
|
||||||
|
{
|
||||||
|
await dbTx.CommitAsync(ct);
|
||||||
|
return BuildResult(purchase, transaction, paymentTx, alreadyPaid: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transaction == null)
|
||||||
|
throw new InvalidOperationException("تراکنش سفارش یافت نشد");
|
||||||
|
|
||||||
|
var alreadyCredited = await _context.UserWalletHistories
|
||||||
|
.AnyAsync(h => h.RefrenceId == transaction.Id && !h.IsDeleted, ct);
|
||||||
|
|
||||||
|
if (alreadyCredited)
|
||||||
|
{
|
||||||
|
transaction.PaymentStatus = PaymentStatus.Success;
|
||||||
|
transaction.PaymentDate ??= DateTime.UtcNow;
|
||||||
|
UpdatePaymentTransaction(paymentTx, verifyResult, markSuccess: true);
|
||||||
|
await _context.SaveChangesAsync(ct);
|
||||||
|
await dbTx.CommitAsync(ct);
|
||||||
|
return BuildResult(purchase, transaction, paymentTx, alreadyPaid: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatePaymentTransaction(paymentTx, verifyResult, markSuccess: true);
|
||||||
|
|
||||||
|
transaction.PaymentStatus = PaymentStatus.Success;
|
||||||
|
transaction.PaymentDate = DateTime.UtcNow;
|
||||||
|
transaction.RefId = verifyResult.RefId;
|
||||||
|
|
||||||
|
var wallet = await _context.UserWallets
|
||||||
|
.FirstOrDefaultAsync(w => w.UserId == purchase.UserId, ct);
|
||||||
|
|
||||||
|
if (wallet == null)
|
||||||
|
{
|
||||||
|
wallet = new UserWallet
|
||||||
|
{
|
||||||
|
UserId = purchase.UserId,
|
||||||
|
Balance = 0,
|
||||||
|
DiscountBalance = 0,
|
||||||
|
NetworkBalance = 0
|
||||||
|
};
|
||||||
|
_context.UserWallets.Add(wallet);
|
||||||
|
await _context.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
var discountMultiplier = purchase.Package?.DiscountMultiplier
|
||||||
|
?? throw new InvalidOperationException("پکیج سفارش یافت نشد");
|
||||||
|
var discountAmount = (long)(purchase.Amount * (double)discountMultiplier);
|
||||||
|
wallet.Balance += purchase.Amount;
|
||||||
|
wallet.DiscountBalance += discountAmount;
|
||||||
|
|
||||||
|
_context.UserWalletHistories.Add(new UserWalletHistory
|
||||||
|
{
|
||||||
|
WalletId = wallet.Id,
|
||||||
|
CurrentBalance = wallet.Balance,
|
||||||
|
ChangeValue = purchase.Amount,
|
||||||
|
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||||
|
ChangeNerworkValue = 0,
|
||||||
|
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||||
|
ChangeDiscountValue = discountAmount,
|
||||||
|
IsIncrease = true,
|
||||||
|
RefrenceId = transaction.Id,
|
||||||
|
PackageId = purchase.PackageId
|
||||||
|
});
|
||||||
|
|
||||||
|
var user = await _context.Users
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == purchase.UserId, ct);
|
||||||
|
if (user != null)
|
||||||
|
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(ct);
|
||||||
|
await dbTx.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await dbTx.RollbackAsync(ct);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _sender.Send(new ActivateClubMembershipCommand
|
||||||
|
{
|
||||||
|
UserId = purchase.UserId,
|
||||||
|
ForceActivation = false
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"Auto club activation failed after package verify. UserId={UserId}, PurchaseId={PurchaseId}",
|
||||||
|
purchase.UserId,
|
||||||
|
purchase.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Package purchase verified. PurchaseId={PurchaseId}, UserId={UserId}, Authority={Authority}",
|
||||||
|
purchase.Id,
|
||||||
|
purchase.UserId,
|
||||||
|
request.Authority);
|
||||||
|
|
||||||
|
return BuildResult(
|
||||||
|
purchase, transaction, paymentTx, alreadyPaid: false, verifyResult.RefId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPaymentAlreadyCompleted(Transaction? transaction, PaymentTransaction? paymentTx)
|
||||||
|
{
|
||||||
|
return transaction?.PaymentStatus == PaymentStatus.Success
|
||||||
|
|| paymentTx?.PaymentStatus == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdatePaymentTransaction(
|
||||||
|
PaymentTransaction? paymentTx,
|
||||||
|
PaymentVerificationResult verifyResult,
|
||||||
|
bool markSuccess)
|
||||||
|
{
|
||||||
|
if (paymentTx == null) return;
|
||||||
|
|
||||||
|
paymentTx.PaymentStatus = markSuccess && verifyResult.IsSuccess;
|
||||||
|
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
|
||||||
|
paymentTx.VerificationStatusMessage = verifyResult.Message;
|
||||||
|
paymentTx.CardPan = verifyResult.CardPan;
|
||||||
|
paymentTx.CardHash = verifyResult.CardHash;
|
||||||
|
paymentTx.RefId = verifyResult.TrackingCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VerifyUserPackagePurchasePaymentResult BuildResult(
|
||||||
|
UserPackagePurchase purchase,
|
||||||
|
Transaction? transaction,
|
||||||
|
PaymentTransaction? paymentTx,
|
||||||
|
bool alreadyPaid,
|
||||||
|
string? referenceCode = null)
|
||||||
|
{
|
||||||
|
var refCode = referenceCode
|
||||||
|
?? paymentTx?.RefId
|
||||||
|
?? transaction?.RefId
|
||||||
|
?? string.Empty;
|
||||||
|
|
||||||
|
return new VerifyUserPackagePurchasePaymentResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
AlreadyPaid = alreadyPaid,
|
||||||
|
Message = alreadyPaid ? "پرداخت قبلاً تأیید شده بود" : "خرید پکیج با موفقیت تایید شد",
|
||||||
|
TransactionId = transaction?.Id ?? 0,
|
||||||
|
ReferenceCode = refCode,
|
||||||
|
PackageId = purchase.PackageId,
|
||||||
|
PackageName = purchase.Package?.Title ?? string.Empty,
|
||||||
|
AmountPaid = purchase.Amount,
|
||||||
|
PurchasedAt = purchase.PurchasedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
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");
|
||||||
|
|
||||||
|
return new GetAllPaymentTransactionByFilterResponseDto
|
||||||
|
{
|
||||||
|
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||||
|
Models = await query
|
||||||
|
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||||
|
.Select(x => new GetAllPaymentTransactionByFilterResponseModel
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
UserId = x.UserId,
|
||||||
|
Mobile = x.Mobile,
|
||||||
|
GatewayProvider = x.GatewayProvider,
|
||||||
|
Amount = x.Amount,
|
||||||
|
Authority = x.Authority,
|
||||||
|
PaymentStatus = x.PaymentStatus,
|
||||||
|
VerificationStatusCode = x.VerificationStatusCode,
|
||||||
|
VerificationStatusMessage = x.VerificationStatusMessage,
|
||||||
|
RefId = x.RefId,
|
||||||
|
CardPan = x.CardPan,
|
||||||
|
OrderId = x.OrderId,
|
||||||
|
TransactionId = x.TransactionId,
|
||||||
|
Description = x.Description,
|
||||||
|
Created = x.Created
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+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? SaleCount { get; set; }
|
||||||
public int? ViewCount { get; set; }
|
public int? ViewCount { get; set; }
|
||||||
public int? RemainingCount { get; set; }
|
public int? RemainingCount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// true = فقط موجود (RemainingCount > 0)، false = فقط ناموجود
|
||||||
|
/// </summary>
|
||||||
|
public bool? InStock { get; set; }
|
||||||
public bool? IsActive { get; set; }
|
public bool? IsActive { get; set; }
|
||||||
public List<long>? CategoryIds { get; set; }
|
public List<long>? CategoryIds { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-5
@@ -56,17 +56,19 @@ public class GetCustomerProductsByFilterQueryHandler : IRequestHandler<GetCustom
|
|||||||
if (request.RemainingCount.HasValue)
|
if (request.RemainingCount.HasValue)
|
||||||
query = query.Where(x => x.RemainingCount == request.RemainingCount.Value);
|
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())
|
if (request.CategoryIds != null && request.CategoryIds.Any())
|
||||||
query = query.Where(x => x.ProductCategories.Any(pc => request.CategoryIds.Contains(pc.CategoryId)));
|
query = query.Where(x => x.ProductCategories.Any(pc => request.CategoryIds.Contains(pc.CategoryId)));
|
||||||
|
|
||||||
if (request.IsActive.HasValue)
|
if (request.IsActive.HasValue)
|
||||||
query = query.Where(x => x.IsDeleted != request.IsActive.Value);
|
query = query.Where(x => x.IsDeleted != request.IsActive.Value);
|
||||||
|
|
||||||
// Apply sorting
|
// موجودیدارها اول، سپس سورت درخواستی
|
||||||
if (!string.IsNullOrEmpty(request.SortBy))
|
query = query.ApplyOrderWithInStockPriority(request.SortBy, x => x.RemainingCount > 0);
|
||||||
query = query.ApplyOrder(request.SortBy);
|
|
||||||
else
|
|
||||||
query = query.OrderByDescending(x => x.Created);
|
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|||||||
+4
@@ -26,4 +26,8 @@ public record GetAllTransactionsByFilterQuery : IRequest<GetAllTransactionsByFil
|
|||||||
public string? RefId { get; set; }
|
public string? RefId { get; set; }
|
||||||
//
|
//
|
||||||
public TransactionType? Type { 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; }
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-16
@@ -1,4 +1,5 @@
|
|||||||
namespace CMSMicroservice.Application.TransactionsCQ.Queries.GetAllTransactionsByFilter;
|
namespace CMSMicroservice.Application.TransactionsCQ.Queries.GetAllTransactionsByFilter;
|
||||||
|
|
||||||
public class GetAllTransactionsByFilterQueryHandler : IRequestHandler<GetAllTransactionsByFilterQuery, GetAllTransactionsByFilterResponseDto>
|
public class GetAllTransactionsByFilterQueryHandler : IRequestHandler<GetAllTransactionsByFilterQuery, GetAllTransactionsByFilterResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
@@ -11,26 +12,27 @@ public class GetAllTransactionsByFilterQueryHandler : IRequestHandler<GetAllTran
|
|||||||
public async Task<GetAllTransactionsByFilterResponseDto> Handle(GetAllTransactionsByFilterQuery request, CancellationToken cancellationToken)
|
public async Task<GetAllTransactionsByFilterResponseDto> Handle(GetAllTransactionsByFilterQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var query = _context.Transactions
|
var query = _context.Transactions
|
||||||
.ApplyOrder(sortBy: request.SortBy)
|
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsQueryable();
|
.ApplyTransactionFilter(request.Filter)
|
||||||
if (request.Filter is not null)
|
.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
||||||
{
|
|
||||||
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)
|
|
||||||
;
|
|
||||||
}
|
|
||||||
return new GetAllTransactionsByFilterResponseDto
|
return new GetAllTransactionsByFilterResponseDto
|
||||||
{
|
{
|
||||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||||
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
|
Models = await query
|
||||||
.ProjectToType<GetAllTransactionsByFilterResponseModel>().ToListAsync(cancellationToken)
|
.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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -24,4 +24,5 @@ public class GetAllTransactionsByFilterResponseDto
|
|||||||
public string? RefId { get; set; }
|
public string? RefId { get; set; }
|
||||||
//
|
//
|
||||||
public TransactionType Type { get; set; }
|
public TransactionType Type { get; set; }
|
||||||
|
public DateTime Created { 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
-6
@@ -81,13 +81,9 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
|
|||||||
if (isMasterCode)
|
if (isMasterCode)
|
||||||
return new VerifyOtpTokenResponseDto { Success = false, Message = "کاربری با این شماره موبایل وجود ندارد." };
|
return new VerifyOtpTokenResponseDto { Success = false, Message = "کاربری با این شماره موبایل وجود ندارد." };
|
||||||
|
|
||||||
// لاگین با شمارهای که ثبتنام نشده → ثبتنام مجاز نیست
|
// کد معرف ارائه نشده → ثبتنام ممکن نیست (ادمین هرگز referral نمیفرستد، مشتری جدید هم اگر نفرستاده یعنی اشتباه وارد شده)
|
||||||
if (purpose == "login")
|
|
||||||
return new VerifyOtpTokenResponseDto { Success = false, Message = "کاربری با این شماره موبایل وجود ندارد." };
|
|
||||||
|
|
||||||
// کد معرف الزامی است
|
|
||||||
if (string.IsNullOrWhiteSpace(request.ParentReferralCode))
|
if (string.IsNullOrWhiteSpace(request.ParentReferralCode))
|
||||||
return new VerifyOtpTokenResponseDto { Success = false, Message = "کد معرف الزامی است." };
|
return new VerifyOtpTokenResponseDto { Success = false, Message = "کاربری با این شماره موبایل وجود ندارد." };
|
||||||
|
|
||||||
// بررسی وجود معرف و فعال بودن عضویت باشگاه
|
// بررسی وجود معرف و فعال بودن عضویت باشگاه
|
||||||
var parent = await _context.Users
|
var parent = await _context.Users
|
||||||
|
|||||||
+28
-3
@@ -34,14 +34,39 @@ public class GetAllUserByFilterQueryHandler : IRequestHandler<GetAllUserByFilter
|
|||||||
.Where(x => request.Filter.SmsNotifications == null || x.SmsNotifications == request.Filter.SmsNotifications)
|
.Where(x => request.Filter.SmsNotifications == null || x.SmsNotifications == request.Filter.SmsNotifications)
|
||||||
.Where(x => request.Filter.EmailNotifications == null || x.EmailNotifications == request.Filter.EmailNotifications)
|
.Where(x => request.Filter.EmailNotifications == null || x.EmailNotifications == request.Filter.EmailNotifications)
|
||||||
.Where(x => request.Filter.PushNotifications == null || x.PushNotifications == request.Filter.PushNotifications)
|
.Where(x => request.Filter.PushNotifications == null || x.PushNotifications == request.Filter.PushNotifications)
|
||||||
.Where(x => request.Filter.BirthDate == null || x.BirthDate == request.Filter.BirthDate)
|
.Where(x => request.Filter.BirthDate == null || x.BirthDate == request.Filter.BirthDate);
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new GetAllUserByFilterResponseDto
|
return new GetAllUserByFilterResponseDto
|
||||||
{
|
{
|
||||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||||
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
|
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
|
||||||
.ProjectToType<GetAllUserByFilterResponseModel>().ToListAsync(cancellationToken)
|
.Select(x => new GetAllUserByFilterResponseModel
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
FirstName = x.FirstName,
|
||||||
|
LastName = x.LastName,
|
||||||
|
Mobile = x.Mobile,
|
||||||
|
NationalCode = x.NationalCode,
|
||||||
|
AvatarPath = x.AvatarPath,
|
||||||
|
NetworkParentId = x.NetworkParentId,
|
||||||
|
ReferralCode = x.ReferralCode,
|
||||||
|
IsMobileVerified = x.IsMobileVerified,
|
||||||
|
MobileVerifiedAt = x.MobileVerifiedAt,
|
||||||
|
EmailNotifications = x.EmailNotifications,
|
||||||
|
SmsNotifications = x.SmsNotifications,
|
||||||
|
PushNotifications = x.PushNotifications,
|
||||||
|
BirthDate = x.BirthDate,
|
||||||
|
DefaultAddress = x.UserAddresses
|
||||||
|
.Where(a => a.IsDefault && !a.IsDeleted)
|
||||||
|
.Select(a => a.Address)
|
||||||
|
.FirstOrDefault() ?? string.Empty,
|
||||||
|
DefaultPostalCode = x.UserAddresses
|
||||||
|
.Where(a => a.IsDefault && !a.IsDeleted)
|
||||||
|
.Select(a => a.PostalCode)
|
||||||
|
.FirstOrDefault() ?? string.Empty
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -36,4 +36,8 @@ public class GetAllUserByFilterResponseDto
|
|||||||
public bool PushNotifications { get; set; }
|
public bool PushNotifications { get; set; }
|
||||||
//تاریخ تولد
|
//تاریخ تولد
|
||||||
public DateTime? BirthDate { get; set; }
|
public DateTime? BirthDate { get; set; }
|
||||||
|
//آدرس پیشفرض
|
||||||
|
public string DefaultAddress { get; set; } = string.Empty;
|
||||||
|
//کدپستی پیشفرض
|
||||||
|
public string DefaultPostalCode { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
using CMSMicroservice.Application.Common.Exceptions;
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities;
|
using CMSMicroservice.Domain.Entities;
|
||||||
@@ -50,8 +51,10 @@ public class GetCustomerOrderQueryHandler : IRequestHandler<GetCustomerOrderQuer
|
|||||||
DeliveryStatus = order.DeliveryStatus,
|
DeliveryStatus = order.DeliveryStatus,
|
||||||
TrackingCode = order.TrackingCode ?? "",
|
TrackingCode = order.TrackingCode ?? "",
|
||||||
DeliveryDescription = order.DeliveryDescription ?? "",
|
DeliveryDescription = order.DeliveryDescription ?? "",
|
||||||
UserFullName = $"{order.User?.FirstName ?? ""} {order.User?.LastName ?? ""}".Trim(),
|
UserFullName = UserDisplayName.From(order.User),
|
||||||
UserNationalCode = order.User?.NationalCode ?? "",
|
UserNationalCode = order.User?.NationalCode ?? "",
|
||||||
|
PostalCode = order.UserAddress?.PostalCode ?? "",
|
||||||
|
UserMobile = order.User?.Mobile ?? "",
|
||||||
VatAmount = order.OrderVAT?.VATAmount ?? 0,
|
VatAmount = order.OrderVAT?.VATAmount ?? 0,
|
||||||
VatPercentage = order.OrderVAT != null ? (double)order.OrderVAT.VATRate * 100 : 0,
|
VatPercentage = order.OrderVAT != null ? (double)order.OrderVAT.VATRate * 100 : 0,
|
||||||
FactorDetails = order.FactorDetails?.Select(fd => new FactorDetailDto
|
FactorDetails = order.FactorDetails?.Select(fd => new FactorDetailDto
|
||||||
|
|||||||
+2
@@ -20,6 +20,8 @@ public class GetCustomerOrderResponseDto
|
|||||||
public string DeliveryDescription { get; set; }
|
public string DeliveryDescription { get; set; }
|
||||||
public string UserFullName { get; set; }
|
public string UserFullName { get; set; }
|
||||||
public string UserNationalCode { get; set; }
|
public string UserNationalCode { get; set; }
|
||||||
|
public string PostalCode { get; set; } = string.Empty;
|
||||||
|
public string UserMobile { get; set; } = string.Empty;
|
||||||
public long VatAmount { get; set; }
|
public long VatAmount { get; set; }
|
||||||
public double VatPercentage { get; set; }
|
public double VatPercentage { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -113,6 +113,7 @@ public class GetCustomerOrderHistoryQueryHandler : IRequestHandler<GetCustomerOr
|
|||||||
DeliveryStatus.Delivered => 4,
|
DeliveryStatus.Delivered => 4,
|
||||||
DeliveryStatus.Cancelled => 5,
|
DeliveryStatus.Cancelled => 5,
|
||||||
DeliveryStatus.Returned => 6,
|
DeliveryStatus.Returned => 6,
|
||||||
|
DeliveryStatus.ReadyForOfficePickup => 7,
|
||||||
_ => 0
|
_ => 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -127,6 +128,7 @@ public class GetCustomerOrderHistoryQueryHandler : IRequestHandler<GetCustomerOr
|
|||||||
DeliveryStatus.Delivered => "تحویل داده شد",
|
DeliveryStatus.Delivered => "تحویل داده شد",
|
||||||
DeliveryStatus.Cancelled => "لغو شده",
|
DeliveryStatus.Cancelled => "لغو شده",
|
||||||
DeliveryStatus.Returned => "مرجوع شده",
|
DeliveryStatus.Returned => "مرجوع شده",
|
||||||
|
DeliveryStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
|
||||||
_ => "نامشخص"
|
_ => "نامشخص"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
using CMSMicroservice.Application.Common.Extensions;
|
using CMSMicroservice.Application.Common.Extensions;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Application.Common.Models;
|
using CMSMicroservice.Application.Common.Models;
|
||||||
@@ -72,7 +73,7 @@ public class GetCustomerOrdersQueryHandler : IRequestHandler<GetCustomerOrdersQu
|
|||||||
DeliveryStatus = order.DeliveryStatus,
|
DeliveryStatus = order.DeliveryStatus,
|
||||||
TrackingCode = order.TrackingCode ?? "",
|
TrackingCode = order.TrackingCode ?? "",
|
||||||
DeliveryDescription = order.DeliveryDescription ?? "",
|
DeliveryDescription = order.DeliveryDescription ?? "",
|
||||||
UserFullName = $"{order.User?.FirstName ?? ""} {order.User?.LastName ?? ""}".Trim(),
|
UserFullName = UserDisplayName.From(order.User),
|
||||||
UserNationalCode = order.User?.NationalCode ?? "",
|
UserNationalCode = order.User?.NationalCode ?? "",
|
||||||
VatAmount = order.OrderVAT?.VATAmount ?? 0,
|
VatAmount = order.OrderVAT?.VATAmount ?? 0,
|
||||||
VatPercentage = order.OrderVAT != null ? (double)order.OrderVAT.VATRate * 100 : 0,
|
VatPercentage = order.OrderVAT != null ? (double)order.OrderVAT.VATRate * 100 : 0,
|
||||||
|
|||||||
+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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-4
@@ -8,12 +8,9 @@ public class GetAllUserWalletByFilterResponseDto
|
|||||||
|
|
||||||
}public class GetAllUserWalletByFilterResponseModel
|
}public class GetAllUserWalletByFilterResponseModel
|
||||||
{
|
{
|
||||||
//شناسه
|
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
//شناسه کاربر
|
|
||||||
public long UserId { get; set; }
|
public long UserId { get; set; }
|
||||||
//موجودی
|
|
||||||
public long Balance { get; set; }
|
public long Balance { get; set; }
|
||||||
//موجودی شبکه
|
|
||||||
public long NetworkBalance { get; set; }
|
public long NetworkBalance { get; set; }
|
||||||
|
public long DiscountBalance { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -20,6 +20,10 @@ public class GetAllUserWalletHistoryByFilterResponseDto
|
|||||||
public long CurrentNetworkBalance { get; set; }
|
public long CurrentNetworkBalance { get; set; }
|
||||||
//تغییر موجودی شبکه
|
//تغییر موجودی شبکه
|
||||||
public long ChangeNerworkValue { get; set; }
|
public long ChangeNerworkValue { get; set; }
|
||||||
|
//موجودی جاری اعتباری
|
||||||
|
public long CurrentDiscountBalance { get; set; }
|
||||||
|
//تغییر موجودی اعتباری
|
||||||
|
public long ChangeDiscountValue { get; set; }
|
||||||
//افزایشی؟
|
//افزایشی؟
|
||||||
public bool IsIncrease { get; set; }
|
public bool IsIncrease { get; set; }
|
||||||
//شناسه ارجاع
|
//شناسه ارجاع
|
||||||
|
|||||||
+4
@@ -13,6 +13,10 @@ public class GetUserWalletHistoryResponseDto
|
|||||||
public long CurrentNetworkBalance { get; set; }
|
public long CurrentNetworkBalance { get; set; }
|
||||||
//تغییر موجودی شبکه
|
//تغییر موجودی شبکه
|
||||||
public long ChangeNerworkValue { get; set; }
|
public long ChangeNerworkValue { get; set; }
|
||||||
|
//موجودی جاری اعتباری
|
||||||
|
public long CurrentDiscountBalance { get; set; }
|
||||||
|
//تغییر موجودی اعتباری
|
||||||
|
public long ChangeDiscountValue { get; set; }
|
||||||
//افزایشی؟
|
//افزایشی؟
|
||||||
public bool IsIncrease { 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("مبلغ وارد شده بیش از حد مجاز است");
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-2
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
using CMSMicroservice.Application.Common.Exceptions;
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Application.Common.Models;
|
using CMSMicroservice.Application.Common.Models;
|
||||||
@@ -17,20 +18,32 @@ public class ChargeDiscountWalletCommandHandler
|
|||||||
private readonly IPaymentGatewayService _paymentGateway;
|
private readonly IPaymentGatewayService _paymentGateway;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<ChargeDiscountWalletCommandHandler> _logger;
|
private readonly ILogger<ChargeDiscountWalletCommandHandler> _logger;
|
||||||
|
private readonly IUserPaymentLock _paymentLock;
|
||||||
|
|
||||||
public ChargeDiscountWalletCommandHandler(
|
public ChargeDiscountWalletCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IPaymentGatewayService paymentGateway,
|
IPaymentGatewayService paymentGateway,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<ChargeDiscountWalletCommandHandler> logger)
|
ILogger<ChargeDiscountWalletCommandHandler> logger,
|
||||||
|
IUserPaymentLock paymentLock)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_paymentGateway = paymentGateway;
|
_paymentGateway = paymentGateway;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_paymentLock = paymentLock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PaymentInitiateResult> Handle(
|
public Task<PaymentInitiateResult> Handle(
|
||||||
|
ChargeDiscountWalletCommand request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
_paymentLock.ExecuteAsync(
|
||||||
|
PaymentLockScopes.Initiate(request.UserId),
|
||||||
|
PaymentLockStrategy.FailFast,
|
||||||
|
ct => HandleCore(request, ct),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
private async Task<PaymentInitiateResult> HandleCore(
|
||||||
ChargeDiscountWalletCommand request,
|
ChargeDiscountWalletCommand request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|||||||
+15
-2
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common;
|
||||||
using CMSMicroservice.Application.Common.Exceptions;
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Common;
|
using CMSMicroservice.Domain.Common;
|
||||||
@@ -18,20 +19,32 @@ public class ChargeMagicWalletCommandHandler
|
|||||||
private readonly IPaymentGatewayService _paymentGateway;
|
private readonly IPaymentGatewayService _paymentGateway;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<ChargeMagicWalletCommandHandler> _logger;
|
private readonly ILogger<ChargeMagicWalletCommandHandler> _logger;
|
||||||
|
private readonly IUserPaymentLock _paymentLock;
|
||||||
|
|
||||||
public ChargeMagicWalletCommandHandler(
|
public ChargeMagicWalletCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IPaymentGatewayService paymentGateway,
|
IPaymentGatewayService paymentGateway,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<ChargeMagicWalletCommandHandler> logger)
|
ILogger<ChargeMagicWalletCommandHandler> logger,
|
||||||
|
IUserPaymentLock paymentLock)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_paymentGateway = paymentGateway;
|
_paymentGateway = paymentGateway;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_paymentLock = paymentLock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PaymentInitiateResult> Handle(
|
public Task<PaymentInitiateResult> Handle(
|
||||||
|
ChargeMagicWalletCommand request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
_paymentLock.ExecuteAsync(
|
||||||
|
PaymentLockScopes.Initiate(request.UserId),
|
||||||
|
PaymentLockStrategy.FailFast,
|
||||||
|
ct => HandleCore(request, ct),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
private async Task<PaymentInitiateResult> HandleCore(
|
||||||
ChargeMagicWalletCommand request,
|
ChargeMagicWalletCommand request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user