Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb81aff095 | |||
| 1a0bb83c99 | |||
| 1e7c17f090 | |||
| 01073084ab | |||
| ed2b20a98e | |||
| 721661af0f | |||
| aee7b59fc1 | |||
| 61b7e4f8f2 | |||
| e5bc3a952a | |||
| 10d2ca20d1 | |||
| fdbb91d2e1 | |||
| a1024a3e18 | |||
| 1ac23667e2 | |||
| dcd11351a3 | |||
| aaaf7fc1ca | |||
| 7554d7054a | |||
| ce8e248843 | |||
| 8446e0e3b5 | |||
| 161f796cd4 | |||
| 469d97bb60 | |||
| d19c569aae | |||
| 7176fe44ee | |||
| 607f791b65 | |||
| 0002a5a6f2 | |||
| ccb938e9ba | |||
| 8e5c7c5205 | |||
| a9cd2fd67a | |||
| ae92ab8697 | |||
| fe3edd178d | |||
| 13dd0f552f | |||
| 8b9c317de6 | |||
| cbaa20f339 | |||
| e206b71186 | |||
| 0457ef6c7c | |||
| f3ac5ad7df | |||
| e72673c184 | |||
| 9288d0640b | |||
| de83c31346 | |||
| f8dc4abd04 | |||
| 2d6c95e6ee | |||
| e41747afe0 | |||
| 68da3f45a3 | |||
| 3153fd8a74 | |||
| 8cdb197ee7 | |||
| 2ca3fd29f9 |
@@ -0,0 +1,102 @@
|
||||
name: Build and Deploy to Kubernetes
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- kub-stage
|
||||
|
||||
env:
|
||||
REGISTRY: 194.5.195.53:30080
|
||||
IMAGE_NAME: admin/cms
|
||||
K8S_SERVER: 194.5.195.53
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 194.5.195.53:32082/docker-sshpass:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- name: Start Docker daemon
|
||||
run: |
|
||||
mkdir -p /etc/docker
|
||||
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||
{
|
||||
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
|
||||
}
|
||||
DAEMON
|
||||
echo "🚀 Starting Docker daemon..."
|
||||
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
|
||||
|
||||
# Wait up to 3 minutes for Docker to be ready
|
||||
for i in $(seq 1 90); do
|
||||
if docker info >/dev/null 2>&1; then
|
||||
echo "✅ Docker daemon is ready (attempt $i)"
|
||||
docker version
|
||||
break
|
||||
else
|
||||
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
|
||||
# Final check
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "❌ Docker daemon failed to start after 3 minutes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
run: |
|
||||
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/CMS.git .
|
||||
|
||||
- name: Login to Docker registries
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||
|
||||
- name: Publish Protobuf packages
|
||||
run: |
|
||||
echo "📦 Publishing Protobuf packages..."
|
||||
docker run --rm --network host -v $(pwd):/src -w /src \
|
||||
194.5.195.53:32082/dotnet/sdk:9.0 sh -c '
|
||||
for proj in $(find . -name "*Protobuf*.csproj" -type f); do
|
||||
echo "📦 $proj"
|
||||
dotnet restore "$proj" --configfile src/NuGet.config
|
||||
dotnet build "$proj" -c Release --no-restore
|
||||
dotnet pack "$proj" -c Release --no-build -o "$(dirname $proj)/nupkg"
|
||||
for nupkg in $(dirname $proj)/nupkg/*.nupkg; do
|
||||
[ -f "$nupkg" ] && dotnet nuget push "$nupkg" \
|
||||
--source "http://194.5.195.53:32081/repository/foursat-nuget-hosted/index.json" \
|
||||
--api-key "admin:87zH26nbqT" \
|
||||
--skip-duplicate --allow-insecure-connections || true
|
||||
done
|
||||
done
|
||||
'
|
||||
echo "✅ Protobuf packages done!"
|
||||
|
||||
- name: Build Docker Image
|
||||
run: |
|
||||
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest .
|
||||
|
||||
- name: Push to Registry
|
||||
run: |
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Deploy to Kubernetes
|
||||
run: |
|
||||
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
|
||||
|
||||
# Copy K8s manifests to server
|
||||
sshpass -e scp -o StrictHostKeyChecking=no k8s/staging/cms-config.yaml root@${{ env.K8S_SERVER }}:/tmp/cms-config.yaml
|
||||
sshpass -e scp -o StrictHostKeyChecking=no k8s/staging/cms-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/cms-deployment.yaml
|
||||
|
||||
# Apply config (Secret) first, then deployment
|
||||
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
||||
kubectl apply -f /tmp/cms-config.yaml &&
|
||||
kubectl apply -f /tmp/cms-deployment.yaml &&
|
||||
kubectl rollout restart deployment/cms &&
|
||||
kubectl rollout status deployment/cms --timeout=180s &&
|
||||
rm -f /tmp/cms-config.yaml /tmp/cms-deployment.yaml
|
||||
"
|
||||
echo "✅ Deployed!"
|
||||
@@ -4,7 +4,7 @@ apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: cms-uploads-pvc
|
||||
namespace: foursat
|
||||
namespace: default
|
||||
labels:
|
||||
app: cms
|
||||
spec:
|
||||
@@ -20,7 +20,7 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: cms
|
||||
namespace: foursat
|
||||
namespace: default
|
||||
labels:
|
||||
app: cms
|
||||
spec:
|
||||
@@ -89,7 +89,7 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: cms-svc
|
||||
namespace: foursat
|
||||
namespace: default
|
||||
spec:
|
||||
selector:
|
||||
app: cms
|
||||
@@ -105,7 +105,7 @@ apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: cms-ingress
|
||||
namespace: foursat
|
||||
namespace: default
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
|
||||
+82
-36
@@ -135,13 +135,25 @@ public class AcceptClubMembershipContractCommandHandler
|
||||
};
|
||||
await _context.UserContracts.AddAsync(userContract, cancellationToken);
|
||||
|
||||
// 6. دریافت مقادیر از SystemConstants (استاتیک)
|
||||
long giftValue = SystemConstants.ClubMembershipGiftValue;
|
||||
long activationFeeValue = SystemConstants.ClubActivationFee;
|
||||
// 6. بارگذاری پکیج از آخرین خرید کاربر یا پکیج پایه
|
||||
var latestPurchase = await _context.UserPackagePurchases
|
||||
.Where(p => p.UserId == userId)
|
||||
.OrderByDescending(p => p.PurchasedAt)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
var package = latestPurchase != null
|
||||
? await _context.Packages.FirstOrDefaultAsync(p => p.Id == latestPurchase.PackageId && !p.IsDeleted, cancellationToken)
|
||||
: await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken);
|
||||
|
||||
if (package == null)
|
||||
throw new NotFoundException("پکیج یافت نشد");
|
||||
|
||||
long giftValue = package.ActivationFee;
|
||||
long activationFeeValue = package.ActivationFee;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Using Club.MembershipGiftValue: {GiftValue}, Club.ActivationFee: {ActivationFee}",
|
||||
giftValue, activationFeeValue
|
||||
"Using Package {PackageId} ({PackageName}): GiftValue={GiftValue}, ActivationFee={ActivationFee}",
|
||||
package.Id, package.Title, giftValue, activationFeeValue
|
||||
);
|
||||
|
||||
// 7. فعالسازی باشگاه مشتریان
|
||||
@@ -156,6 +168,10 @@ public class AcceptClubMembershipContractCommandHandler
|
||||
UserId = user.Id,
|
||||
IsActive = true,
|
||||
ActivatedAt = activationDate,
|
||||
FirstActivationDate = activationDate,
|
||||
FirstPackageId = package.Id,
|
||||
LastActivationDate = activationDate,
|
||||
LastPackageId = package.Id,
|
||||
InitialContribution = activationFeeValue,
|
||||
GiftValue = giftValue,
|
||||
TotalEarned = 0,
|
||||
@@ -175,6 +191,8 @@ public class AcceptClubMembershipContractCommandHandler
|
||||
clubMembership = user.ClubMembership!;
|
||||
clubMembership.IsActive = true;
|
||||
clubMembership.ActivatedAt = activationDate;
|
||||
clubMembership.LastActivationDate = activationDate;
|
||||
clubMembership.LastPackageId = package.Id;
|
||||
clubMembership.PurchaseMethod = user.PackagePurchaseMethod;
|
||||
_context.ClubMemberships.Update(clubMembership);
|
||||
|
||||
@@ -206,13 +224,14 @@ public class AcceptClubMembershipContractCommandHandler
|
||||
// 9. اضافه کردن مبلغ به Pool هفته جاری
|
||||
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
|
||||
var weeklyPool = await _context.WeeklyCommissionPools
|
||||
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId, cancellationToken);
|
||||
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
|
||||
|
||||
if (weeklyPool == null)
|
||||
{
|
||||
weeklyPool = new WeeklyCommissionPool
|
||||
{
|
||||
WeekDefinitionId = currentWeekDefinitionId,
|
||||
PackageId = package.Id,
|
||||
TotalPoolAmount = activationFeeValue,
|
||||
TotalBalances = 0,
|
||||
ValuePerBalance = 0,
|
||||
@@ -243,36 +262,8 @@ public class AcceptClubMembershipContractCommandHandler
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 10. اعطای ویژگیهای باشگاه برای کاربر (فقط برای عضویت جدید)
|
||||
if (isNewMembership)
|
||||
{
|
||||
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
|
||||
var clubFeatures = await _context.ClubFeatures
|
||||
.Where(f => !f.IsDeleted && featureIds.Contains(f.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (clubFeatures.Any())
|
||||
{
|
||||
var userClubFeatures = clubFeatures.Select(feature => new UserClubFeature
|
||||
{
|
||||
UserId = user.Id,
|
||||
ClubMembershipId = clubMembership.Id,
|
||||
ClubFeatureId = feature.Id,
|
||||
GrantedAt = activationDate,
|
||||
IsActive = true,
|
||||
Notes = "اعطا شده هنگام امضای قرارداد"
|
||||
}).ToList();
|
||||
|
||||
_context.UserClubFeatures.AddRange(userClubFeatures);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Granted {Count} club features to UserId {UserId}",
|
||||
clubFeatures.Count,
|
||||
user.Id
|
||||
);
|
||||
}
|
||||
}
|
||||
// 10. اعمال ویژگیهای پکیج — DIFF/تفاضل (Q20)
|
||||
await ApplyFeatureDiffAsync(user.Id, clubMembership.Id, package.Id, isNewMembership, activationDate, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Club membership contract accepted and activated for UserId: {UserId}, ContractId: {ContractId}, MembershipId: {MembershipId}",
|
||||
@@ -302,6 +293,61 @@ public class AcceptClubMembershipContractCommandHandler
|
||||
return week.Id;
|
||||
}
|
||||
|
||||
private async Task ApplyFeatureDiffAsync(
|
||||
long userId, long membershipId, long packageId,
|
||||
bool isNewMembership, DateTime activationDate,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var newFeatureIds = await _context.PackageFeatures
|
||||
.Where(pf => pf.PackageId == packageId && pf.IsIncluded && !pf.IsDeleted)
|
||||
.Select(pf => pf.ClubFeatureId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (!newFeatureIds.Any())
|
||||
{
|
||||
_logger.LogWarning("No PackageFeatures for PackageId {PackageId}", packageId);
|
||||
return;
|
||||
}
|
||||
|
||||
var currentFeatures = await _context.UserClubFeatures
|
||||
.Where(ucf => ucf.UserId == userId && ucf.IsActive)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var currentFeatureIds = currentFeatures.Select(f => f.ClubFeatureId).ToHashSet();
|
||||
var toAdd = newFeatureIds.Where(id => !currentFeatureIds.Contains(id)).ToList();
|
||||
var toRemove = currentFeatures.Where(f => !newFeatureIds.Contains(f.ClubFeatureId)).ToList();
|
||||
|
||||
foreach (var feature in toRemove)
|
||||
{
|
||||
feature.IsActive = false;
|
||||
feature.Notes = $"حذف شده بابت تغییر پکیج (PackageId: {packageId})";
|
||||
}
|
||||
|
||||
if (toAdd.Any())
|
||||
{
|
||||
var newUserFeatures = toAdd.Select(featureId => new UserClubFeature
|
||||
{
|
||||
UserId = userId,
|
||||
ClubMembershipId = membershipId,
|
||||
ClubFeatureId = featureId,
|
||||
GrantedAt = activationDate,
|
||||
IsActive = true,
|
||||
Notes = isNewMembership
|
||||
? "اعطا شده هنگام امضای قرارداد"
|
||||
: $"اضافه شده بابت خرید مجدد پکیج (PackageId: {packageId})"
|
||||
}).ToList();
|
||||
|
||||
_context.UserClubFeatures.AddRange(newUserFeatures);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Feature DIFF applied for UserId={UserId}: Added={Added}, Removed={Removed}, Kept={Kept}",
|
||||
userId, toAdd.Count, toRemove.Count,
|
||||
currentFeatureIds.Count - toRemove.Count);
|
||||
}
|
||||
|
||||
private async Task<(bool Success, string Message)> VerifyOtpAsync(
|
||||
string mobile,
|
||||
string code,
|
||||
|
||||
+5
@@ -12,4 +12,9 @@ public record ActivateClubMembershipCommand : IRequest<bool>
|
||||
/// شناسه کاربر
|
||||
/// </summary>
|
||||
public long UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// فعالسازی اجباری توسط ادمین — بدون بررسی سفارش و کیفپول
|
||||
/// </summary>
|
||||
public bool ForceActivation { get; init; }
|
||||
}
|
||||
|
||||
+165
-85
@@ -54,15 +54,21 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
throw new NotFoundException(nameof(User), request.UserId);
|
||||
}
|
||||
|
||||
// متغیر پکیج — در هر دو مسیر (عادی و Force) مقداردهی میشود
|
||||
Package package;
|
||||
|
||||
// 2-5: بررسیهای مالی — در حالت ForceActivation (ادمین) رد میشود
|
||||
if (!request.ForceActivation)
|
||||
{
|
||||
// 2. بررسی اینکه پکیج خریده باشد
|
||||
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"User {UserId} has not purchased golden package yet",
|
||||
"User {UserId} has not purchased any package yet",
|
||||
request.UserId
|
||||
);
|
||||
throw new BadRequestException(
|
||||
"برای فعالسازی باشگاه مشتریان ابتدا باید پکیج طلایی خریداری کنید"
|
||||
"برای فعالسازی باشگاه مشتریان ابتدا باید یک پکیج خریداری کنید"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,17 +82,8 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
throw new NotFoundException("کیف پول کاربر یافت نشد");
|
||||
}
|
||||
|
||||
if (wallet.Balance < SystemConstants.BasePackageAmount)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"User {UserId} has insufficient balance: {Balance}",
|
||||
request.UserId,
|
||||
wallet.Balance
|
||||
);
|
||||
throw new BadRequestException(
|
||||
$"برای فعالسازی باشگاه مشتریان باید حداقل {SystemConstants.BasePackageAmount:N0} ریال موجودی اصلی داشته باشید"
|
||||
);
|
||||
}
|
||||
// NOTE: balance check uses package.Price — loaded after finding order (step 4)
|
||||
// Moved to after package loading
|
||||
|
||||
// 3.5. بررسی وضعیت کیفپول جادویی — اگر در حالت Magic است، فعالسازی مجاز نیست
|
||||
if (wallet.WalletMode == WalletMode.Magic)
|
||||
@@ -100,43 +97,44 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
);
|
||||
}
|
||||
|
||||
// 4. پیدا کردن UserOrder با PackageId
|
||||
var packageOrder = await _context.UserOrders
|
||||
.Include(o => o.Transaction)
|
||||
.Where(o =>
|
||||
o.UserId == user.Id &&
|
||||
o.PackageId != null &&
|
||||
o.PaymentStatus == PaymentStatus.Success)
|
||||
.OrderByDescending(o => o.Created)
|
||||
// 4. پیدا کردن آخرین خرید موفق پکیج
|
||||
var packagePurchase = await _context.UserPackagePurchases
|
||||
.Include(p => p.Transaction)
|
||||
.Where(p =>
|
||||
p.UserId == user.Id &&
|
||||
p.Transaction != null &&
|
||||
p.Transaction.PaymentStatus == PaymentStatus.Success)
|
||||
.OrderByDescending(p => p.Created)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (packageOrder == null)
|
||||
if (packagePurchase == null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"No successful package order found for UserId: {UserId}",
|
||||
"No successful package purchase found for UserId: {UserId}",
|
||||
request.UserId
|
||||
);
|
||||
throw new NotFoundException("سفارش پکیج طلایی یافت نشد");
|
||||
throw new NotFoundException("سفارش پکیج یافت نشد");
|
||||
}
|
||||
|
||||
// 5. بررسی Transaction
|
||||
if (packageOrder.Transaction == null)
|
||||
if (packagePurchase.Transaction == null)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Transaction not found for OrderId: {OrderId}",
|
||||
packageOrder.Id
|
||||
"Transaction not found for PurchaseId: {PurchaseId}",
|
||||
packagePurchase.Id
|
||||
);
|
||||
throw new NotFoundException("تراکنش مربوط به سفارش یافت نشد");
|
||||
}
|
||||
|
||||
var transaction = packageOrder.Transaction;
|
||||
var transaction = packagePurchase.Transaction;
|
||||
|
||||
if (transaction.Type != TransactionType.DepositIpg &&
|
||||
if (transaction.Type != TransactionType.Buy &&
|
||||
transaction.Type != TransactionType.DepositIpg &&
|
||||
transaction.Type != TransactionType.DepositExternal1)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Invalid transaction type for OrderId {OrderId}: {Type}",
|
||||
packageOrder.Id,
|
||||
"Invalid transaction type for PurchaseId {PurchaseId}: {Type}",
|
||||
packagePurchase.Id,
|
||||
transaction.Type
|
||||
);
|
||||
throw new BadRequestException(
|
||||
@@ -144,17 +142,51 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
);
|
||||
}
|
||||
|
||||
// 5.5. بارگذاری پکیج از خرید
|
||||
package = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.Id == packagePurchase.PackageId && !p.IsDeleted, cancellationToken)
|
||||
?? throw new NotFoundException("پکیج یافت نشد");
|
||||
|
||||
// بررسی موجودی با مبلغ پکیج واقعی
|
||||
if (wallet.Balance < package.Price)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
$"برای فعالسازی باشگاه مشتریان باید حداقل {package.Price:N0} تومان موجودی اصلی داشته باشید"
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Force activation by admin for UserId: {UserId} — skipping order/wallet validation",
|
||||
request.UserId
|
||||
);
|
||||
|
||||
// در فعالسازی اجباری، اگر PackagePurchaseMethod تنظیم نشده، مقدار DirectPurchase بگذار
|
||||
if (user.PackagePurchaseMethod == PackagePurchaseMethod.None)
|
||||
{
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
||||
_context.Users.Update(user);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// بارگذاری پکیج پایه برای فعالسازی اجباری
|
||||
package = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
|
||||
?? throw new NotFoundException("پکیج پایه یافت نشد");
|
||||
}
|
||||
|
||||
// 6. بررسی عضویت فعلی
|
||||
var existingMembership = await _context.ClubMemberships
|
||||
.FirstOrDefaultAsync(c => c.UserId == user.Id, cancellationToken);
|
||||
|
||||
// 6.1. دریافت مبلغ هدیه و هزینه فعالسازی از SystemConstants
|
||||
long giftValue = SystemConstants.ClubMembershipGiftValue;
|
||||
long activationFeeValue = SystemConstants.ClubActivationFee;
|
||||
// 6.1. دریافت مبلغ هدیه و هزینه فعالسازی از پکیج
|
||||
long giftValue = package.ActivationFee;
|
||||
long activationFeeValue = package.ActivationFee;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Using Club.MembershipGiftValue: {GiftValue}, Club.ActivationFee: {ActivationFee}",
|
||||
giftValue, activationFeeValue
|
||||
"Using Package {PackageId} ({PackageName}): GiftValue={GiftValue}, ActivationFee={ActivationFee}, Price={Price}",
|
||||
package.Id, package.Title, giftValue, activationFeeValue, package.Price
|
||||
);
|
||||
|
||||
ClubMembership entity;
|
||||
@@ -163,14 +195,18 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
|
||||
if (isNewMembership)
|
||||
{
|
||||
// ایجاد عضویت جدید
|
||||
// ایجاد عضویت جدید — IsActive = false تا زمان امضای قرارداد باشگاه
|
||||
entity = new ClubMembership
|
||||
{
|
||||
UserId = user.Id,
|
||||
IsActive = true,
|
||||
IsActive = false,
|
||||
ActivatedAt = activationDate,
|
||||
InitialContribution =activationFeeValue,
|
||||
GiftValue = giftValue, // مقدار از تنظیمات
|
||||
FirstActivationDate = activationDate,
|
||||
FirstPackageId = package.Id,
|
||||
LastActivationDate = activationDate,
|
||||
LastPackageId = package.Id,
|
||||
InitialContribution = activationFeeValue,
|
||||
GiftValue = giftValue,
|
||||
TotalEarned = 0,
|
||||
PurchaseMethod = user.PackagePurchaseMethod
|
||||
};
|
||||
@@ -186,25 +222,32 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
}
|
||||
else
|
||||
{
|
||||
if (existingMembership.IsActive)
|
||||
// بررسی آیا چرخه فعال دارد — اگر بله، واقعاً فعال است و نیازی به کار نیست
|
||||
var hasCurrentCycle = await _context.ClubMembershipCycles
|
||||
.AnyAsync(c => c.UserId == user.Id && c.IsCurrentCycle, cancellationToken);
|
||||
|
||||
if (existingMembership.IsActive && hasCurrentCycle)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"User {UserId} is already an active club member",
|
||||
"User {UserId} is already an active club member with a running cycle",
|
||||
user.Id
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// فعالسازی مجدد — ActivatedAt حفظ میشه (overwrite نمیشه)
|
||||
// فعالسازی مجدد یا خرید مجدد (Q19/Q20) — FirstActivation حفظ میشه (Q21), Last بروزرسانی میشه
|
||||
// مقدار IsActive قبلی حفظ میشه — اگر قبلاً فعال بوده، فعال بمونه
|
||||
entity = existingMembership;
|
||||
entity.IsActive = true;
|
||||
entity.LastActivationDate = activationDate;
|
||||
entity.LastPackageId = package.Id;
|
||||
entity.PurchaseMethod = user.PackagePurchaseMethod;
|
||||
|
||||
_context.ClubMemberships.Update(entity);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Reactivated club membership for UserId {UserId}",
|
||||
user.Id
|
||||
"Re-activating club membership for UserId {UserId} (re-purchase scenario, IsActive was {WasActive})",
|
||||
user.Id,
|
||||
existingMembership.IsActive
|
||||
);
|
||||
}
|
||||
|
||||
@@ -231,7 +274,8 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
CycleNumber = maxCycleNumber + 1,
|
||||
PackagePurchasedAt = activationDate,
|
||||
PurchaseMethod = user.PackagePurchaseMethod,
|
||||
PackageAmount = SystemConstants.BasePackageAmount,
|
||||
PackageAmount = package.Price,
|
||||
PackageId = package.Id,
|
||||
IsCurrentCycle = true
|
||||
};
|
||||
|
||||
@@ -265,15 +309,16 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
// ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری
|
||||
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
|
||||
var weeklyPool = await _context.WeeklyCommissionPools
|
||||
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId, cancellationToken);
|
||||
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
|
||||
|
||||
if (weeklyPool == null)
|
||||
{
|
||||
// ایجاد Pool جدید برای این هفته
|
||||
// ایجاد Pool جدید برای این هفته × این پکیج
|
||||
weeklyPool = new WeeklyCommissionPool
|
||||
{
|
||||
WeekDefinitionId = currentWeekDefinitionId,
|
||||
TotalPoolAmount = activationFeeValue, // مبلغ هدیه به Pool اضافه میشه
|
||||
PackageId = package.Id,
|
||||
TotalPoolAmount = activationFeeValue,
|
||||
TotalBalances = 0, // در CalculateWeeklyBalances محاسبه میشه
|
||||
ValuePerBalance = 0, // در CalculateWeeklyCommissionPool محاسبه میشه
|
||||
IsCalculated = false,
|
||||
@@ -304,41 +349,8 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 9. اضافه کردن ویژگیهای باشگاه برای کاربر (فقط برای عضویت جدید)
|
||||
if (isNewMembership)
|
||||
{
|
||||
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
|
||||
var clubFeatures = await _context.ClubFeatures
|
||||
.Where(f => !f.IsDeleted && featureIds.Contains(f.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (clubFeatures.Any())
|
||||
{
|
||||
var userClubFeatures = clubFeatures.Select(feature => new UserClubFeature
|
||||
{
|
||||
UserId = user.Id,
|
||||
ClubMembershipId = entity.Id,
|
||||
ClubFeatureId = feature.Id,
|
||||
GrantedAt = activationDate,
|
||||
IsActive = true,
|
||||
Notes = "اعطا شده بهطور خودکار هنگام فعالسازی"
|
||||
}).ToList(); _context.UserClubFeatures.AddRange(userClubFeatures);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Granted {Count} club features to UserId {UserId}",
|
||||
clubFeatures.Count,
|
||||
user.Id
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"No club features found to grant to UserId {UserId}",
|
||||
user.Id
|
||||
);
|
||||
}
|
||||
}
|
||||
// 9. اعمال ویژگیهای پکیج — DIFF/تفاضل (Q20)
|
||||
await ApplyFeatureDiffAsync(user.Id, entity.Id, package.Id, isNewMembership, activationDate, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Club membership activated successfully. UserId: {UserId}, MembershipId: {MembershipId}",
|
||||
@@ -359,6 +371,74 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// اعمال DIFF فیچرها بر اساس پکیج (Q20)
|
||||
/// عضویت جدید: همه فیچرهای پکیج اضافه میشه
|
||||
/// خرید مجدد: تفاضل فیچرهای فعلی و پکیج جدید
|
||||
/// </summary>
|
||||
private async Task ApplyFeatureDiffAsync(
|
||||
long userId, long membershipId, long packageId,
|
||||
bool isNewMembership, DateTime activationDate,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// فیچرهای پکیج جدید
|
||||
var newFeatureIds = await _context.PackageFeatures
|
||||
.Where(pf => pf.PackageId == packageId && pf.IsIncluded && !pf.IsDeleted)
|
||||
.Select(pf => pf.ClubFeatureId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (!newFeatureIds.Any())
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"No PackageFeatures found for PackageId {PackageId} — skipping feature assignment for UserId {UserId}",
|
||||
packageId, userId);
|
||||
return;
|
||||
}
|
||||
|
||||
// فیچرهای فعال فعلی کاربر
|
||||
var currentFeatures = await _context.UserClubFeatures
|
||||
.Where(ucf => ucf.UserId == userId && ucf.IsActive)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var currentFeatureIds = currentFeatures.Select(f => f.ClubFeatureId).ToHashSet();
|
||||
|
||||
// DIFF
|
||||
var toAdd = newFeatureIds.Where(id => !currentFeatureIds.Contains(id)).ToList();
|
||||
var toRemove = currentFeatures.Where(f => !newFeatureIds.Contains(f.ClubFeatureId)).ToList();
|
||||
|
||||
// حذف فیچرهای قدیمی که پکیج جدید ندارد
|
||||
foreach (var feature in toRemove)
|
||||
{
|
||||
feature.IsActive = false;
|
||||
feature.Notes = $"حذف شده بابت تغییر پکیج (PackageId: {packageId})";
|
||||
}
|
||||
|
||||
// اضافه فیچرهای جدید
|
||||
if (toAdd.Any())
|
||||
{
|
||||
var newUserFeatures = toAdd.Select(featureId => new UserClubFeature
|
||||
{
|
||||
UserId = userId,
|
||||
ClubMembershipId = membershipId,
|
||||
ClubFeatureId = featureId,
|
||||
GrantedAt = activationDate,
|
||||
IsActive = true,
|
||||
Notes = isNewMembership
|
||||
? "اعطا شده هنگام فعالسازی اولیه"
|
||||
: $"اضافه شده بابت خرید مجدد پکیج (PackageId: {packageId})"
|
||||
}).ToList();
|
||||
|
||||
_context.UserClubFeatures.AddRange(newUserFeatures);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Feature DIFF applied for UserId={UserId}: Added={Added}, Removed={Removed}, Kept={Kept}",
|
||||
userId, toAdd.Count, toRemove.Count,
|
||||
currentFeatureIds.Count - toRemove.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت شناسه تعریف هفته جاری
|
||||
/// </summary>
|
||||
|
||||
+9
-5
@@ -82,11 +82,14 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
||||
var balancesList = new List<NetworkWeeklyBalance>();
|
||||
var calculatedAt = DateTime.Now;
|
||||
|
||||
// استفاده از SystemConstants (استاتیک - بدون کوئری به دیتابیس)
|
||||
// سقف تعادل هفتگی برای هر دست (نه کل) - 300 برای چپ + 300 برای راست = حداکثر 600 تعادل
|
||||
var maxBalancesPerLeg = SystemConstants.CommissionMaxWeeklyBalancesPerLeg;
|
||||
// حداکثر عمق شبکه برای شمارش اعضا (15 لول)
|
||||
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
|
||||
// بارگذاری پکیج پایه برای خواندن سقفها
|
||||
var package = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
|
||||
?? throw new InvalidOperationException("پکیج پایه یافت نشد");
|
||||
|
||||
// خواندن سقفها از پکیج (قبلاً از SystemConstants بود)
|
||||
var maxBalancesPerLeg = package.MaxBalancesPerLeg;
|
||||
var maxNetworkLevel = package.MaxNetworkLevel;
|
||||
|
||||
foreach (var user in usersInNetwork.OrderBy(o=>o.Id))
|
||||
{
|
||||
@@ -140,6 +143,7 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
|
||||
{
|
||||
UserId = user.Id,
|
||||
WeekDefinitionId = request.WeekDefinitionId,
|
||||
PackageId = package.Id,
|
||||
|
||||
// اطلاعات جدید
|
||||
LeftLegNewMembers = leftNewMembers,
|
||||
|
||||
+5
-5
@@ -192,7 +192,7 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
|
||||
.ToDictionaryAsync(w => w.UserId, cancellationToken);
|
||||
|
||||
var newWallets = new List<UserWallet>();
|
||||
var walletLogs = new List<UserWalletChangeLog>();
|
||||
var walletLogs = new List<UserWalletHistory>();
|
||||
|
||||
foreach (var payout in payouts)
|
||||
{
|
||||
@@ -241,7 +241,7 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
|
||||
wallet.NetworkBalance += payout.TotalAmount;
|
||||
|
||||
// ایجاد لاگ تغییر کیف پول
|
||||
var walletLog = new UserWalletChangeLog
|
||||
var walletLog = new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
@@ -258,7 +258,7 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
|
||||
}
|
||||
|
||||
// ذخیره تغییرات کیف پول و لاگها
|
||||
await _context.UserWalletChangeLogs.AddRangeAsync(walletLogs, cancellationToken);
|
||||
await _context.UserWalletHistories.AddRangeAsync(walletLogs, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// پیدا کردن لاگهای کیف پول مرتبط با پرداختهای قبلی
|
||||
var oldWalletLogs = await _context.UserWalletChangeLogs
|
||||
var oldWalletLogs = await _context.UserWalletHistories
|
||||
.Where(l => l.RefrenceId.HasValue && oldPayoutIds.Contains(l.RefrenceId.Value))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -300,7 +300,7 @@ public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<Calcu
|
||||
}
|
||||
|
||||
// حذف لاگهای قبلی
|
||||
_context.UserWalletChangeLogs.RemoveRange(oldWalletLogs);
|
||||
_context.UserWalletHistories.RemoveRange(oldWalletLogs);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
+6
-2
@@ -49,8 +49,11 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// ⭐ خواندن MaxNetworkLevel از SystemConstants (استاتیک)
|
||||
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
|
||||
// بارگذاری پکیج پایه برای خواندن سقفها
|
||||
var package = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
|
||||
?? throw new InvalidOperationException("پکیج پایه یافت نشد");
|
||||
var maxNetworkLevel = package.MaxNetworkLevel;
|
||||
|
||||
// دریافت همه تعادلهای هفتگی (شامل صفرها هم برای محاسبه زیرمجموعه)
|
||||
var allWeeklyBalances = await _context.NetworkWeeklyBalances
|
||||
@@ -113,6 +116,7 @@ public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayou
|
||||
UserId = userId,
|
||||
WeekDefinitionId = request.WeekDefinitionId,
|
||||
WeeklyPoolId = pool.Id,
|
||||
PackageId = package.Id,
|
||||
BalancesEarned = totalBalancesWithSubordinates, // ⭐ شامل زیرمجموعه
|
||||
ValuePerBalance = pool.ValuePerBalance,
|
||||
TotalAmount = totalAmount,
|
||||
|
||||
+5
@@ -18,6 +18,11 @@ public record GetMyCommissionPayoutsQuery : IRequest<GetMyCommissionPayoutsRespo
|
||||
/// </summary>
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// فیلتر بر اساس پکیج (اختیاری)
|
||||
/// </summary>
|
||||
public long? PackageId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pagination
|
||||
/// </summary>
|
||||
|
||||
+10
-1
@@ -28,6 +28,7 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissio
|
||||
|
||||
var query = _context.UserCommissionPayouts
|
||||
.Include(x => x.WeekDefinition)
|
||||
.Include(x => x.Package)
|
||||
.Where(x => x.UserId == userId)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
@@ -43,6 +44,12 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissio
|
||||
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId.Value);
|
||||
}
|
||||
|
||||
// فیلتر بر اساس پکیج
|
||||
if (request.PackageId.HasValue && request.PackageId.Value > 0)
|
||||
{
|
||||
query = query.Where(x => x.PackageId == request.PackageId.Value);
|
||||
}
|
||||
|
||||
// مرتبسازی: جدیدترین اول
|
||||
query = query.OrderByDescending(x => x.Created);
|
||||
|
||||
@@ -60,7 +67,9 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissio
|
||||
AmountFormatted = x.TotalAmount.ToString("N0") + " تومان",
|
||||
Status = x.Status,
|
||||
CalculatedDate = x.PaidAt ?? (DateTime?)x.Created,
|
||||
DatePersian = ""
|
||||
DatePersian = "",
|
||||
PackageId = x.PackageId,
|
||||
PackageTitle = x.Package != null ? x.Package.Title : ""
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
|
||||
+4
@@ -20,4 +20,8 @@ public class GetMyCommissionPayoutsResponseModel
|
||||
public CommissionPayoutStatus Status { get; set; }
|
||||
public DateTime? CalculatedDate { get; set; }
|
||||
public string DatePersian { get; set; } = string.Empty;
|
||||
|
||||
// Package info
|
||||
public long PackageId { get; set; }
|
||||
public string PackageTitle { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public record GetMyWeeklyBalancesQuery : IRequest<GetUserWeeklyBalancesResponseD
|
||||
/// </summary>
|
||||
public bool OnlyActive { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// فیلتر بر اساس پکیج (اختیاری)
|
||||
/// </summary>
|
||||
public long? PackageId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pagination
|
||||
/// </summary>
|
||||
|
||||
+1
@@ -38,6 +38,7 @@ public class GetMyWeeklyBalancesQueryHandler : IRequestHandler<GetMyWeeklyBalanc
|
||||
{
|
||||
UserId = userId,
|
||||
WeekDefinitionId = request.WeekDefinitionId,
|
||||
PackageId = request.PackageId,
|
||||
OnlyActive = request.OnlyActive,
|
||||
PaginationState = request.PaginationState
|
||||
};
|
||||
|
||||
+5
@@ -20,6 +20,11 @@ public record GetUserCommissionPayoutsQuery : IRequest<GetUserCommissionPayoutsR
|
||||
/// </summary>
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// فیلتر بر اساس پکیج (اختیاری)
|
||||
/// </summary>
|
||||
public long? PackageId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// مرتبسازی
|
||||
/// </summary>
|
||||
|
||||
+10
-1
@@ -18,6 +18,7 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
||||
var query = _context.UserCommissionPayouts
|
||||
.Include(x => x.WeekDefinition)
|
||||
.Include(x => x.User)
|
||||
.Include(x => x.Package)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
@@ -41,6 +42,12 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
||||
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId);
|
||||
}
|
||||
|
||||
// فیلتر بر اساس پکیج
|
||||
if (request.PackageId.HasValue && request.PackageId.Value > 0)
|
||||
{
|
||||
query = query.Where(x => x.PackageId == request.PackageId.Value);
|
||||
}
|
||||
|
||||
query = query.ApplyOrder(sortBy: request.SortBy ?? "Created");
|
||||
|
||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||
@@ -64,7 +71,9 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
||||
WithdrawalMethod = x.WithdrawalMethod,
|
||||
IbanNumber = x.IbanNumber,
|
||||
WithdrawnAt = x.WithdrawnAt,
|
||||
Created = x.Created
|
||||
Created = x.Created,
|
||||
PackageId = x.PackageId,
|
||||
PackageTitle = x.Package != null ? x.Package.Title : ""
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
|
||||
+4
@@ -24,4 +24,8 @@ public class GetUserCommissionPayoutsResponseModel
|
||||
public string? IbanNumber { get; set; }
|
||||
public DateTime? WithdrawnAt { get; set; }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
|
||||
// Package info
|
||||
public long PackageId { get; set; }
|
||||
public string PackageTitle { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
+4
@@ -15,6 +15,10 @@ public record GetUserWeeklyBalancesQuery : IRequest<GetUserWeeklyBalancesRespons
|
||||
/// </summary>
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// فیلتر بر اساس پکیج (اختیاری)
|
||||
/// </summary>
|
||||
public long? PackageId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// فقط موارد Expired نشده؟
|
||||
|
||||
+10
-1
@@ -18,6 +18,7 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
||||
var query = _context.NetworkWeeklyBalances
|
||||
.Include(x => x.WeekDefinition)
|
||||
.Include(x => x.User)
|
||||
.Include(x => x.Package)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
@@ -43,6 +44,12 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
||||
query = query.Where(x => !x.IsExpired);
|
||||
}
|
||||
|
||||
// فیلتر بر اساس پکیج
|
||||
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");
|
||||
|
||||
@@ -67,7 +74,9 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
||||
WeeklyPoolContribution = x.WeeklyPoolContribution,
|
||||
CalculatedAt = x.CalculatedAt,
|
||||
IsExpired = x.IsExpired,
|
||||
Created = x.Created
|
||||
Created = x.Created,
|
||||
PackageId = x.PackageId,
|
||||
PackageTitle = x.Package != null ? x.Package.Title : ""
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
|
||||
+4
@@ -29,4 +29,8 @@ public class GetUserWeeklyBalancesResponseModel
|
||||
public DateTime? CalculatedAt { get; set; }
|
||||
public bool IsExpired { get; set; }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
|
||||
// Package info
|
||||
public long PackageId { get; set; }
|
||||
public string PackageTitle { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
+11
-1
@@ -51,6 +51,15 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
|
||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// دریافت PackageName از آخرین خرید پکیج کاربران
|
||||
var userIds = models.Select(x => x.UserId).Distinct().ToList();
|
||||
var userPackageNames = await _context.UserPackagePurchases
|
||||
.Where(p => userIds.Contains(p.UserId))
|
||||
.Include(p => p.Package)
|
||||
.GroupBy(p => p.UserId)
|
||||
.Select(g => new { UserId = g.Key, PackageName = g.OrderByDescending(x => x.PurchasedAt).First().Package.Title })
|
||||
.ToDictionaryAsync(x => x.UserId, x => x.PackageName, cancellationToken);
|
||||
|
||||
var result = models.Select(x => new WithdrawalRequestModel
|
||||
{
|
||||
Id = x.Id,
|
||||
@@ -69,7 +78,8 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
|
||||
BankReferenceId = x.BankReferenceId,
|
||||
BankTrackingCode = x.BankTrackingCode,
|
||||
PaymentFailureReason = x.PaymentFailureReason,
|
||||
Created = x.Created
|
||||
Created = x.Created,
|
||||
PackageName = userPackageNames.GetValueOrDefault(x.UserId)
|
||||
}).ToList();
|
||||
|
||||
return new GetWithdrawalRequestsResponseDto
|
||||
|
||||
+1
@@ -25,4 +25,5 @@ public class WithdrawalRequestModel
|
||||
public string? BankTrackingCode { get; set; }
|
||||
public string? PaymentFailureReason { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public string? PackageName { get; set; }
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public interface IUserNotificationService
|
||||
long userId,
|
||||
decimal amount,
|
||||
int weekNumber,
|
||||
string? packageName = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -42,6 +43,7 @@ public interface IUserNotificationService
|
||||
/// </summary>
|
||||
Task SendClubActivationNotificationAsync(
|
||||
long userId,
|
||||
string? packageName = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -50,5 +52,6 @@ public interface IUserNotificationService
|
||||
Task SendPayoutErrorNotificationAsync(
|
||||
long userId,
|
||||
string errorMessage,
|
||||
string? packageName = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -32,13 +32,16 @@ public interface IApplicationDbContext
|
||||
DbSet<OrderVAT> OrderVATs { get; }
|
||||
DbSet<UserPackagePurchase> UserPackagePurchases { get; }
|
||||
DbSet<UserWallet> UserWallets { get; }
|
||||
DbSet<UserWalletChangeLog> UserWalletChangeLogs { get; }
|
||||
DbSet<UserWalletHistory> UserWalletHistories { get; }
|
||||
DbSet<ManualPayment> ManualPayments { get; }
|
||||
DbSet<PaymentTransaction> PaymentTransactions { get; }
|
||||
DbSet<PublicMessage> PublicMessages { get; }
|
||||
DbSet<ClubMembership> ClubMemberships { get; }
|
||||
DbSet<ClubMembershipHistory> ClubMembershipHistories { get; }
|
||||
DbSet<ClubMembershipCycle> ClubMembershipCycles { get; }
|
||||
DbSet<ClubMembershipCycleHistory> ClubMembershipCycleHistories { get; }
|
||||
DbSet<PackageHistory> PackageHistories { get; }
|
||||
DbSet<PackageFeature> PackageFeatures { get; }
|
||||
DbSet<ClubFeature> ClubFeatures { get; }
|
||||
DbSet<UserClubFeature> UserClubFeatures { get; }
|
||||
DbSet<NetworkWeeklyBalance> NetworkWeeklyBalances { get; }
|
||||
|
||||
@@ -13,5 +13,10 @@ public interface IKavenegarService
|
||||
/// <summary>
|
||||
/// ارسال پیامک با قالب (VerifyLookup)
|
||||
/// </summary>
|
||||
Task VerifyLookupAsync(string mobile, string token, string template = "Afrino");
|
||||
Task VerifyLookupAsync(string mobile, string token, string template = "OTP");
|
||||
|
||||
/// <summary>
|
||||
/// ارسال پیامک با قالب و توکن دوم (VerifyLookup) — مثلاً برای قراردادها
|
||||
/// </summary>
|
||||
Task VerifyLookupAsync(string mobile, string token, string token2, string template);
|
||||
}
|
||||
|
||||
@@ -41,8 +41,10 @@ public interface IPaymentGatewayService
|
||||
decimal amountInToman,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// پیشفرض: درگاههایی که Amount نمیخواهند، از overload بدون amount استفاده کنند
|
||||
return VerifyPaymentAsync(refId, verificationToken, cancellationToken);
|
||||
// ⚠️ هشدار: این پیادهسازی پیشفرض مبلغ را نادیده میگیرد.
|
||||
// درگاههایی مثل زرینپال باید حتماً این متد را override کنند.
|
||||
throw new NotImplementedException(
|
||||
"درگاه پرداخت باید متد VerifyPaymentAsync با مبلغ را پیادهسازی کند");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
using CMSMicroservice.Application.UserWalletChangeLogCQ.Queries.GetAllUserWalletChangeLogByFilter;
|
||||
using CMSMicroservice.Application.UserWalletChangeLogCQ.Queries.GetUserWalletChangeLog;
|
||||
|
||||
namespace CMSMicroservice.Application.Common.Mappings;
|
||||
|
||||
public class UserWalletChangeLogProfile : IRegister
|
||||
{
|
||||
void IRegister.Register(TypeAdapterConfig config)
|
||||
{
|
||||
config.NewConfig<UserWalletChangeLog,GetAllUserWalletChangeLogByFilterResponseModel>()
|
||||
.Map(dest => dest.CreatedAt, src => src.Created);
|
||||
|
||||
config.NewConfig<UserWalletChangeLog, GetUserWalletChangeLogResponseDto>()
|
||||
.Map(dest => dest.CreatedAt, src => src.Created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using CMSMicroservice.Application.UserWalletHistoryCQ.Queries.GetAllUserWalletHistoryByFilter;
|
||||
using CMSMicroservice.Application.UserWalletHistoryCQ.Queries.GetUserWalletHistory;
|
||||
|
||||
namespace CMSMicroservice.Application.Common.Mappings;
|
||||
|
||||
public class UserWalletHistoryProfile : IRegister
|
||||
{
|
||||
void IRegister.Register(TypeAdapterConfig config)
|
||||
{
|
||||
config.NewConfig<UserWalletHistory,GetAllUserWalletHistoryByFilterResponseModel>()
|
||||
.Map(dest => dest.CreatedAt, src => src.Created);
|
||||
|
||||
config.NewConfig<UserWalletHistory, GetUserWalletHistoryResponseDto>()
|
||||
.Map(dest => dest.CreatedAt, src => src.Created);
|
||||
}
|
||||
}
|
||||
+28
-29
@@ -12,7 +12,7 @@ namespace CMSMicroservice.Application.DayaLoanCQ.Commands.CheckAndProcessDayaLoa
|
||||
/// 1. استعلام از API دایا
|
||||
/// 2. ذخیره/بهروزرسانی DayaLoanContract
|
||||
/// 3. شارژ کیف پول برای وامهای تأیید شده
|
||||
/// 4. ثبت Order پکیج طلایی
|
||||
/// 4. ثبت Order پکیج
|
||||
/// 5. ارسال SMS
|
||||
/// </summary>
|
||||
public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndProcessDayaLoansCommand, CheckAndProcessDayaLoansResponseDto>
|
||||
@@ -122,6 +122,7 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
|
||||
contract.IsProcessed = true;
|
||||
result.WasProcessed = true;
|
||||
result.NewWalletBalance = processResult.NewBalance;
|
||||
result.PackageName = processResult.PackageName;
|
||||
result.Message = "وام پردازش و کیف پول شارژ شد";
|
||||
processedCount++;
|
||||
|
||||
@@ -181,15 +182,20 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
|
||||
/// <summary>
|
||||
/// پردازش مالی وام دایا
|
||||
/// </summary>
|
||||
private async Task<(long NewBalance, long TransactionId)> ProcessDayaLoanAsync(
|
||||
private async Task<(long NewBalance, long TransactionId, string PackageName)> ProcessDayaLoanAsync(
|
||||
User user,
|
||||
string contractNumber,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// بارگذاری پکیج پایه
|
||||
var package = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
|
||||
?? throw new InvalidOperationException("پکیج پایه یافت نشد");
|
||||
|
||||
// 1. ایجاد تراکنش
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Amount = SystemConstants.DayaLoanAmount,
|
||||
Amount = package.Price,
|
||||
Description = $"دریافت اعتبار دایا - قرارداد {contractNumber}",
|
||||
PaymentStatus = PaymentStatus.Success,
|
||||
PaymentDate = DateTime.Now,
|
||||
@@ -216,25 +222,26 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
|
||||
}
|
||||
|
||||
// 3. شارژ کیف پول عادی
|
||||
wallet.Balance += SystemConstants.DayaLoanAmount;
|
||||
wallet.Balance += package.Price;
|
||||
|
||||
var mainLog = new UserWalletChangeLog
|
||||
var mainLog = new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = 0,
|
||||
ChangeValue = SystemConstants.DayaLoanAmount,
|
||||
ChangeValue = package.Price,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
RefrenceId = transaction.Id,
|
||||
PackageId = package.Id
|
||||
};
|
||||
await _context.UserWalletChangeLogs.AddAsync(mainLog, cancellationToken);
|
||||
await _context.UserWalletHistories.AddAsync(mainLog, cancellationToken);
|
||||
|
||||
// 4. شارژ کیف پول تخفیف (دو برابر)
|
||||
var discountAmount = SystemConstants.DayaLoanAmount * 2;
|
||||
// 4. شارژ کیف پول تخفیف
|
||||
var discountAmount = (long)(package.Price * package.DiscountMultiplier);
|
||||
wallet.DiscountBalance += discountAmount;
|
||||
|
||||
var discountLog = new UserWalletChangeLog
|
||||
var discountLog = new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
@@ -244,62 +251,54 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
|
||||
CurrentDiscountBalance = 0,
|
||||
ChangeDiscountValue = discountAmount,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
RefrenceId = transaction.Id,
|
||||
PackageId = package.Id
|
||||
};
|
||||
await _context.UserWalletChangeLogs.AddAsync(discountLog, cancellationToken);
|
||||
await _context.UserWalletHistories.AddAsync(discountLog, cancellationToken);
|
||||
|
||||
// 5. بهروزرسانی کاربر
|
||||
user.HasReceivedDayaCredit = true;
|
||||
user.DayaCreditReceivedAt = DateTime.Now;
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DayaLoan;
|
||||
|
||||
// 6. ثبت Order پکیج پایه
|
||||
var goldenPackage = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.Id == 4, cancellationToken);
|
||||
|
||||
if (goldenPackage != null)
|
||||
{
|
||||
// 6. ثبت UserPackagePurchase برای پکیج پایه
|
||||
var goldenPackageId =goldenPackage.Id;
|
||||
var packagePurchase = new UserPackagePurchase
|
||||
{
|
||||
UserId = user.Id,
|
||||
PackageId = goldenPackageId,
|
||||
PackageId = package.Id,
|
||||
PurchaseMethod = PackagePurchaseMethod.DayaLoan,
|
||||
PurchasedAt = DateTime.Now,
|
||||
Amount = SystemConstants.DayaLoanAmount,
|
||||
Amount = package.Price,
|
||||
TransactionId = transaction.Id
|
||||
};
|
||||
|
||||
await _context.UserPackagePurchases.AddAsync(packagePurchase, cancellationToken);
|
||||
|
||||
}
|
||||
|
||||
// 7. Domain Event
|
||||
user.AddDomainEvent(new DayaLoanApprovedEvent(user, transaction, contractNumber));
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 8. ارسال SMS
|
||||
await SendDayaLoanSmsAsync(user);
|
||||
await SendDayaLoanSmsAsync(user, package.Title);
|
||||
|
||||
return (wallet.Balance, transaction.Id);
|
||||
return (wallet.Balance, transaction.Id, package.Title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ارسال SMS اطلاعرسانی
|
||||
/// </summary>
|
||||
private async Task SendDayaLoanSmsAsync(User user)
|
||||
private async Task SendDayaLoanSmsAsync(User user, string packageName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userName = SmsTemplates.GetUserName(user.FirstName+" "+user.LastName);
|
||||
var message = SmsTemplates.DayaLoanReceived(userName);
|
||||
var message = SmsTemplates.DayaLoanReceived(userName, packageName);
|
||||
|
||||
await _smsService.SendAsync(user.Mobile, message);
|
||||
|
||||
// ارسال SMS به ادمین برای اطلاع
|
||||
var adminMessage = $"وام دایا دریافت شد\nکاربر: {user.FirstName} {user.LastName}\nموبایل: {user.Mobile}\nکدملی: {user.NationalCode}";
|
||||
var adminMessage = $"وام دایا دریافت شد\nکاربر: {user.FirstName} {user.LastName}\nموبایل: {user.Mobile}\nکدملی: {user.NationalCode}\nپکیج: {packageName}";
|
||||
await _smsService.SendAsync("09199877503", adminMessage);
|
||||
|
||||
_logger.LogInformation("Daya loan SMS sent to User {UserId}", user.Id);
|
||||
|
||||
+5
@@ -63,6 +63,11 @@ public class DayaLoanProcessResult
|
||||
/// </summary>
|
||||
public long? NewWalletBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نام پکیج
|
||||
/// </summary>
|
||||
public string? PackageName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// پیام
|
||||
/// </summary>
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
|
||||
userWallet.DiscountBalance -= order.DiscountBalanceUsed;
|
||||
|
||||
// ثبت لاگ تغییرات کیف پول
|
||||
_context.UserWalletChangeLogs.Add(new Domain.Entities.UserWalletChangeLog
|
||||
_context.UserWalletHistories.Add(new Domain.Entities.UserWalletHistory
|
||||
{
|
||||
WalletId = userWallet.Id,
|
||||
CurrentBalance = userWallet.Balance,
|
||||
|
||||
+5
-5
@@ -118,7 +118,7 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
|
||||
return new PlaceOrderResponseDto
|
||||
{
|
||||
Success = false,
|
||||
Message = $"موجودی کیف پول اعتباری کافی نیست. مبلغ مورد نیاز: {maxDiscountBalanceUsable:N0} ریال، موجودی شما: {userWallet.DiscountBalance:N0} ریال"
|
||||
Message = $"موجودی کیف پول اعتباری کافی نیست. مبلغ مورد نیاز: {maxDiscountBalanceUsable:N0} تومان، موجودی شما: {userWallet.DiscountBalance:N0} تومان"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -195,9 +195,9 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
|
||||
{
|
||||
try
|
||||
{
|
||||
// آدرس callback — زرینپال بعد از پرداخت کاربر را به اینجا هدایت میکند
|
||||
var cmsBaseUrl = _configuration["CmsBaseUrl"] ?? "https://localhost:32846";
|
||||
var callbackUrl = $"{cmsBaseUrl}/api/payment/discount-order/callback?orderId={order.Id}";
|
||||
// آدرس callback — زرینپال بعد از پرداخت مستقیم به فرانتآفیس هدایت میکند
|
||||
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
|
||||
var callbackUrl = $"{frontOfficeBaseUrl}/profile/payment-callback?type=discount-order&orderId={order.Id}";
|
||||
|
||||
// درخواست به درگاه
|
||||
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
|
||||
@@ -279,7 +279,7 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
|
||||
walletForDeduct.DiscountBalance -= actualDiscountBalanceUsed;
|
||||
|
||||
// ثبت لاگ تغییرات کیف پول
|
||||
_context.UserWalletChangeLogs.Add(new Domain.Entities.UserWalletChangeLog
|
||||
_context.UserWalletHistories.Add(new Domain.Entities.UserWalletHistory
|
||||
{
|
||||
WalletId = walletForDeduct.Id,
|
||||
CurrentBalance = walletForDeduct.Balance,
|
||||
|
||||
+6
-6
@@ -108,7 +108,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
|
||||
wallet.DiscountBalance += manualPayment.Amount;
|
||||
|
||||
// لاگ Balance
|
||||
await _context.UserWalletChangeLogs.AddAsync(new UserWalletChangeLog
|
||||
await _context.UserWalletHistories.AddAsync(new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
@@ -122,7 +122,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
|
||||
}, cancellationToken);
|
||||
|
||||
// لاگ DiscountBalance
|
||||
await _context.UserWalletChangeLogs.AddAsync(new UserWalletChangeLog
|
||||
await _context.UserWalletHistories.AddAsync(new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
@@ -139,7 +139,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
|
||||
case ManualPaymentType.DiscountWalletCharge:
|
||||
wallet.DiscountBalance += manualPayment.Amount;
|
||||
|
||||
await _context.UserWalletChangeLogs.AddAsync(new UserWalletChangeLog
|
||||
await _context.UserWalletHistories.AddAsync(new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
@@ -156,7 +156,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
|
||||
case ManualPaymentType.NetworkWalletCharge:
|
||||
wallet.NetworkBalance += manualPayment.Amount;
|
||||
|
||||
await _context.UserWalletChangeLogs.AddAsync(new UserWalletChangeLog
|
||||
await _context.UserWalletHistories.AddAsync(new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
@@ -183,7 +183,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
|
||||
wallet.DiscountBalance -= manualPayment.Amount;
|
||||
}
|
||||
|
||||
await _context.UserWalletChangeLogs.AddAsync(new UserWalletChangeLog
|
||||
await _context.UserWalletHistories.AddAsync(new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
@@ -201,7 +201,7 @@ public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualP
|
||||
// Other یا سایر موارد - فقط Balance
|
||||
wallet.Balance += manualPayment.Amount;
|
||||
|
||||
await _context.UserWalletChangeLogs.AddAsync(new UserWalletChangeLog
|
||||
await _context.UserWalletHistories.AddAsync(new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ public class CreateManualPaymentCommand : IRequest<long>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// مبلغ تراکنش (ریال)
|
||||
/// مبلغ تراکنش (تومان)
|
||||
/// </summary>
|
||||
public long Amount { get; set; }
|
||||
|
||||
|
||||
+13
-8
@@ -70,9 +70,13 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
|
||||
throw new NotFoundException($"کیف پول کاربر {request.UserId} یافت نشد");
|
||||
}
|
||||
|
||||
// 4. محاسبه مبالغ
|
||||
var balanceAmount = SystemConstants.BasePackageAmount; // 56M
|
||||
var discountBalanceAmount = SystemConstants.BasePackageAmount * 2; // 112M
|
||||
// 4. بارگذاری پکیج پایه و محاسبه مبالغ
|
||||
var package = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
|
||||
?? throw new NotFoundException("پکیج پایه یافت نشد");
|
||||
|
||||
var balanceAmount = package.Price;
|
||||
var discountBalanceAmount = (long)(package.Price * package.DiscountMultiplier);
|
||||
|
||||
|
||||
// 5. ثبت تراکنش
|
||||
@@ -112,11 +116,11 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
|
||||
var oldBalance = wallet.Balance;
|
||||
var oldDiscountBalance = wallet.DiscountBalance;
|
||||
|
||||
wallet.Balance += balanceAmount; // +56M
|
||||
wallet.DiscountBalance += discountBalanceAmount; // +112M
|
||||
wallet.Balance += balanceAmount;
|
||||
wallet.DiscountBalance += discountBalanceAmount;
|
||||
|
||||
// 8. ثبت لاگ کیف پول
|
||||
var walletLog = new UserWalletChangeLog
|
||||
var walletLog = new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = 0,
|
||||
@@ -126,10 +130,11 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
|
||||
CurrentDiscountBalance =0,
|
||||
ChangeDiscountValue = discountBalanceAmount,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
RefrenceId = transaction.Id,
|
||||
PackageId = package.Id
|
||||
};
|
||||
|
||||
await _context.UserWalletChangeLogs.AddAsync(walletLog, cancellationToken);
|
||||
await _context.UserWalletHistories.AddAsync(walletLog, cancellationToken);
|
||||
|
||||
// 9. تنظیم روش خرید پکیج
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
||||
|
||||
+4
-2
@@ -1,3 +1,4 @@
|
||||
using CMSMicroservice.Domain.Common;
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.ManualPaymentCQ.Commands.CreateManualPayment;
|
||||
@@ -10,11 +11,12 @@ public class CreateManualPaymentCommandValidator : AbstractValidator<CreateManua
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
|
||||
|
||||
// حصار ایمنی — مبلغ واقعی از پکیج کاربر خوانده میشود
|
||||
RuleFor(x => x.Amount)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("مبلغ باید بزرگتر از صفر باشد")
|
||||
.LessThanOrEqualTo(1_000_000_000)
|
||||
.WithMessage("مبلغ نمیتواند بیشتر از 1 میلیارد ریال باشد");
|
||||
.LessThanOrEqualTo(SystemConstants.WalletMaxSafeAmount)
|
||||
.WithMessage("مبلغ وارد شده بیش از حد مجاز است");
|
||||
|
||||
RuleFor(x => x.Type)
|
||||
.IsInEnum()
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ public record ProcessManualMembershipPaymentCommand : IRequest<ProcessManualMemb
|
||||
public long UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// مبلغ پرداختی (ریال)
|
||||
/// مبلغ پرداختی (تومان)
|
||||
/// </summary>
|
||||
public long Amount { get; init; }
|
||||
|
||||
|
||||
+2
-2
@@ -104,7 +104,7 @@ public class ProcessManualMembershipPaymentCommandHandler : IRequestHandler<Proc
|
||||
wallet.DiscountBalance += request.Amount;
|
||||
|
||||
// 8. ثبت لاگ Balance
|
||||
var balanceLog = new UserWalletChangeLog
|
||||
var balanceLog = new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
@@ -116,7 +116,7 @@ public class ProcessManualMembershipPaymentCommandHandler : IRequestHandler<Proc
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
};
|
||||
await _context.UserWalletChangeLogs.AddAsync(balanceLog, cancellationToken);
|
||||
await _context.UserWalletHistories.AddAsync(balanceLog, cancellationToken);
|
||||
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
||||
// 10. بهروزرسانی ManualPayment با TransactionId
|
||||
|
||||
+15
@@ -69,6 +69,14 @@ public class GetAllManualPaymentsQueryHandler
|
||||
|
||||
// Pagination
|
||||
var skip = (request.PageNumber - 1) * request.PageSize;
|
||||
|
||||
// دریافت PackageName از آخرین خرید پکیج کاربر
|
||||
var userPackageNames = await _context.UserPackagePurchases
|
||||
.Include(p => p.Package)
|
||||
.GroupBy(p => p.UserId)
|
||||
.Select(g => new { UserId = g.Key, PackageName = g.OrderByDescending(x => x.PurchasedAt).First().Package.Title })
|
||||
.ToDictionaryAsync(x => x.UserId, x => x.PackageName, cancellationToken);
|
||||
|
||||
var data = await query
|
||||
.Skip(skip)
|
||||
.Take(request.PageSize)
|
||||
@@ -96,6 +104,13 @@ public class GetAllManualPaymentsQueryHandler
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// تکمیل PackageName
|
||||
foreach (var item in data)
|
||||
{
|
||||
if (userPackageNames.TryGetValue(item.UserId, out var pkgName))
|
||||
item.PackageName = pkgName;
|
||||
}
|
||||
|
||||
var metaData = new MetaData
|
||||
{
|
||||
TotalCount = totalCount,
|
||||
|
||||
+1
@@ -30,4 +30,5 @@ public class ManualPaymentDto
|
||||
public string? RejectionReason { get; set; }
|
||||
public long? TransactionId { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public string? PackageName { get; set; }
|
||||
}
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@ public class GetUserNetworkPositionQueryHandler : IRequestHandler<GetUserNetwork
|
||||
HasReceivedDayaCredit = user.HasReceivedDayaCredit,
|
||||
DayaCreditReceivedAt = user.DayaCreditReceivedAt,
|
||||
PackagePurchaseMethod = user.PackagePurchaseMethod,
|
||||
HasPurchasedGoldenPackage = user.PackagePurchaseMethod != PackagePurchaseMethod.None,
|
||||
HasPurchasedPackage = user.PackagePurchaseMethod != PackagePurchaseMethod.None,
|
||||
|
||||
// آمار مالی
|
||||
TotalEarnedCommission = commissionStats?.TotalAmount ?? 0,
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ public class UserNetworkPositionDto
|
||||
public bool HasReceivedDayaCredit { get; set; }
|
||||
public DateTime? DayaCreditReceivedAt { get; set; }
|
||||
public PackagePurchaseMethod PackagePurchaseMethod { get; set; }
|
||||
public bool HasPurchasedGoldenPackage { get; set; }
|
||||
public bool HasPurchasedPackage { get; set; }
|
||||
|
||||
// آمار مالی
|
||||
public decimal TotalEarnedCommission { get; set; }
|
||||
|
||||
+2
-2
@@ -60,7 +60,7 @@ public class CancelOrderByAdminCommandHandler : IRequestHandler<CancelOrderByAdm
|
||||
var wallet = order.User.UserWallets.FirstOrDefault();
|
||||
if (wallet != null)
|
||||
{
|
||||
var walletLog = new UserWalletChangeLog
|
||||
var walletLog = new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
@@ -74,7 +74,7 @@ public class CancelOrderByAdminCommandHandler : IRequestHandler<CancelOrderByAdm
|
||||
|
||||
wallet.Balance += order.Amount;
|
||||
|
||||
await _context.UserWalletChangeLogs.AddAsync(walletLog, cancellationToken);
|
||||
await _context.UserWalletHistories.AddAsync(walletLog, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Refund processed. OrderId: {OrderId}, Amount: {Amount}, UserId: {UserId}",
|
||||
|
||||
+3
-3
@@ -26,12 +26,12 @@ public class CreateNewOtpTokenEventHandler : INotificationHandler<CreateNewOtpTo
|
||||
{
|
||||
var purpose = notification.Item.Purpose?.ToLowerInvariant();
|
||||
|
||||
// برای امضای قرارداد، پیامک ساده با GUID ارسال شود
|
||||
// برای امضای قرارداد، از الگوی Sign-Contract با GUID به عنوان token2 استفاده شود
|
||||
if ((purpose == "signcontract" || purpose == "signclubcontract")
|
||||
&& !string.IsNullOrEmpty(notification.SignGuid))
|
||||
{
|
||||
var message = $"کد تایید امضای قرارداد: {notification.PlainCode}\nشناسه قرارداد: {notification.SignGuid}";
|
||||
await _kavenegarService.SendAsync(notification.Item.Mobile, message);
|
||||
await _kavenegarService.VerifyLookupAsync(
|
||||
notification.Item.Mobile, notification.PlainCode, notification.SignGuid, "Sign-Contract");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+14
-1
@@ -9,5 +9,18 @@ public record CreateNewPackageCommand : IRequest<CreateNewPackageResponseDto>
|
||||
public string ImagePath { get; init; }
|
||||
//قیمت
|
||||
public long Price { get; init; }
|
||||
|
||||
// فیلدهای جدید پکیج
|
||||
public int SortOrder { get; init; }
|
||||
public bool IsActive { get; init; } = true;
|
||||
public bool IsBasePackage { get; init; }
|
||||
public bool SupportsDayaPurchase { get; init; } = true;
|
||||
public bool SupportsDirectPurchase { get; init; } = true;
|
||||
public long ActivationFee { get; init; }
|
||||
public double DiscountMultiplier { get; init; } = 2.0;
|
||||
public double MagicWalletMultiplier { get; init; } = 2.5;
|
||||
public int MaxBalancesPerLeg { get; init; } = 300;
|
||||
public int MaxNetworkLevel { get; init; } = 15;
|
||||
public long MagicWalletMaxDeposit { get; init; } = 1_000_000_000;
|
||||
public long MagicWalletMaxCredit { get; init; } = 2_500_000_000;
|
||||
public List<long> FeatureIds { get; init; } = new();
|
||||
}
|
||||
+16
@@ -16,6 +16,22 @@ public class CreateNewPackageCommandHandler : IRequestHandler<CreateNewPackageCo
|
||||
await _context.Packages.AddAsync(entity, cancellationToken);
|
||||
entity.AddDomainEvent(new CreateNewPackageEvent(entity));
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Sync PackageFeatures
|
||||
if (request.FeatureIds.Any())
|
||||
{
|
||||
foreach (var featureId in request.FeatureIds)
|
||||
{
|
||||
_context.PackageFeatures.Add(new PackageFeature
|
||||
{
|
||||
PackageId = entity.Id,
|
||||
ClubFeatureId = featureId,
|
||||
IsIncluded = true
|
||||
});
|
||||
}
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return entity.Adapt<CreateNewPackageResponseDto>();
|
||||
}
|
||||
}
|
||||
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.InitiateBasePackagePayment;
|
||||
|
||||
/// <summary>
|
||||
/// ثبت اولیه تراکنش و سفارش پکیج پایه (56 میلیون تومان)
|
||||
/// این Command توسط BFF فراخوانی میشود قبل از ارسال کاربر به درگاه
|
||||
/// </summary>
|
||||
public record InitiateBasePackagePaymentCommand : IRequest<InitiateBasePackagePaymentResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه کاربر
|
||||
/// </summary>
|
||||
public long UserId { get; init; }
|
||||
}
|
||||
|
||||
public class InitiateBasePackagePaymentResponseDto
|
||||
{
|
||||
/// <summary>
|
||||
/// موفقیت عملیات
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// پیام
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// شناسه سفارش
|
||||
/// </summary>
|
||||
public long OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه تراکنش CMS
|
||||
/// </summary>
|
||||
public long TransactionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// مبلغ پکیج
|
||||
/// </summary>
|
||||
public long Amount { get; set; }
|
||||
}
|
||||
-169
@@ -1,169 +0,0 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using CMSMicroservice.Domain.Common;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ValidationException = FluentValidation.ValidationException;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.InitiateBasePackagePayment;
|
||||
|
||||
/// <summary>
|
||||
/// Handler برای ثبت اولیه تراکنش و سفارش پکیج پایه
|
||||
/// این Handler فقط Transaction و Order را با وضعیت Pending ایجاد میکند
|
||||
/// BFF سپس کاربر را به درگاه PYMS هدایت میکند
|
||||
/// </summary>
|
||||
public class InitiateBasePackagePaymentCommandHandler : IRequestHandler<InitiateBasePackagePaymentCommand, InitiateBasePackagePaymentResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ILogger<InitiateBasePackagePaymentCommandHandler> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// شناسه پکیج پایه در دیتابیس
|
||||
/// </summary>
|
||||
private const long BasePackageId = 4;
|
||||
|
||||
public InitiateBasePackagePaymentCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
ILogger<InitiateBasePackagePaymentCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<InitiateBasePackagePaymentResponseDto> Handle(
|
||||
InitiateBasePackagePaymentCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Initiating base package payment for UserId: {UserId}",
|
||||
request.UserId);
|
||||
|
||||
// 1. پیدا کردن کاربر
|
||||
var user = await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("User not found. UserId: {UserId}", request.UserId);
|
||||
throw new NotFoundException(nameof(User), request.UserId);
|
||||
}
|
||||
|
||||
// 2. بررسی عدم خرید قبلی پکیج پایه
|
||||
if (user.PackagePurchaseMethod != PackagePurchaseMethod.None)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"User {UserId} has already purchased base package via {Method}",
|
||||
request.UserId,
|
||||
user.PackagePurchaseMethod);
|
||||
|
||||
return new InitiateBasePackagePaymentResponseDto
|
||||
{
|
||||
Success = false,
|
||||
Message = "شما قبلاً پکیج پایه را خریداری کردهاید."
|
||||
};
|
||||
}
|
||||
|
||||
// 3. بررسی عدم وجود سفارش Pending
|
||||
var pendingOrder = await _context.UserOrders
|
||||
.FirstOrDefaultAsync(o =>
|
||||
o.UserId == request.UserId &&
|
||||
o.PaymentStatus == PaymentStatus.Pending &&
|
||||
o.PackageId == BasePackageId,
|
||||
cancellationToken);
|
||||
|
||||
if (pendingOrder != null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"User {UserId} has pending order {OrderId}",
|
||||
request.UserId,
|
||||
pendingOrder.Id);
|
||||
|
||||
// برگرداندن سفارش موجود برای ادامه پرداخت
|
||||
var existingTransaction = await _context.Transactions
|
||||
.FirstOrDefaultAsync(t => t.Id == pendingOrder.TransactionId, cancellationToken);
|
||||
|
||||
return new InitiateBasePackagePaymentResponseDto
|
||||
{
|
||||
Success = true,
|
||||
Message = "سفارش قبلی در انتظار پرداخت یافت شد.",
|
||||
OrderId = pendingOrder.Id,
|
||||
TransactionId = existingTransaction?.Id ?? 0,
|
||||
Amount = SystemConstants.BasePackageAmount
|
||||
};
|
||||
}
|
||||
|
||||
// 4. پیدا کردن آدرس پیشفرض کاربر
|
||||
var defaultAddress = await _context.UserAddresses
|
||||
.Where(a => a.UserId == request.UserId)
|
||||
.OrderByDescending(a => a.Created)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (defaultAddress == null)
|
||||
{
|
||||
_logger.LogWarning("No address found for user {UserId}", request.UserId);
|
||||
throw new ValidationException("لطفاً ابتدا یک آدرس برای خود ثبت کنید.");
|
||||
}
|
||||
|
||||
// 5. ایجاد Transaction با وضعیت Pending
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Amount = SystemConstants.BasePackageAmount,
|
||||
Description = $"خرید پکیج پایه ۵۶ میلیونی - کاربر #{user.Id}",
|
||||
PaymentStatus = PaymentStatus.Pending,
|
||||
Type = TransactionType.DepositIpg
|
||||
};
|
||||
|
||||
_context.Transactions.Add(transaction);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created Transaction {TransactionId} with Pending status for UserId {UserId}",
|
||||
transaction.Id,
|
||||
request.UserId);
|
||||
|
||||
// 6. ایجاد UserOrder با وضعیت Pending
|
||||
var order = new UserOrder
|
||||
{
|
||||
UserId = user.Id,
|
||||
PackageId = BasePackageId,
|
||||
Amount = SystemConstants.BasePackageAmount,
|
||||
PaymentStatus = PaymentStatus.Pending,
|
||||
DeliveryStatus = DeliveryStatus.None,
|
||||
UserAddressId = defaultAddress.Id,
|
||||
PaymentMethod = PaymentMethod.IPG,
|
||||
TransactionId = transaction.Id
|
||||
};
|
||||
|
||||
_context.UserOrders.Add(order);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created UserOrder {OrderId} for UserId {UserId}, TransactionId: {TransactionId}",
|
||||
order.Id,
|
||||
request.UserId,
|
||||
transaction.Id);
|
||||
|
||||
return new InitiateBasePackagePaymentResponseDto
|
||||
{
|
||||
Success = true,
|
||||
Message = "تراکنش و سفارش با موفقیت ثبت شد.",
|
||||
OrderId = order.Id,
|
||||
TransactionId = transaction.Id,
|
||||
Amount = SystemConstants.BasePackageAmount
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error in InitiateBasePackagePaymentCommand for UserId: {UserId}",
|
||||
request.UserId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.InitiateBasePackagePayment;
|
||||
|
||||
public class InitiateBasePackagePaymentCommandValidator : AbstractValidator<InitiateBasePackagePaymentCommand>
|
||||
{
|
||||
public InitiateBasePackagePaymentCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.UserId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه کاربر معتبر نیست.");
|
||||
}
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
|
||||
|
||||
/// <summary>
|
||||
/// خرید پکیج طلایی (شروع فرآیند پرداخت)
|
||||
/// </summary>
|
||||
public record PurchaseGoldenPackageCommand : IRequest<PurchaseGoldenPackageResponseDto>
|
||||
{
|
||||
public long UserId { get; init; }
|
||||
public long PackageId { get; init; }
|
||||
public string ReturnUrl { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public class PurchaseGoldenPackageResponseDto
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public long OrderId { get; set; }
|
||||
public string PaymentGatewayUrl { get; set; } = string.Empty;
|
||||
public string TrackingCode { get; set; } = string.Empty;
|
||||
}
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
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;
|
||||
using ValidationException = FluentValidation.ValidationException;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
|
||||
|
||||
public class PurchaseGoldenPackageCommandHandler : IRequestHandler<PurchaseGoldenPackageCommand, PurchaseGoldenPackageResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IPaymentGatewayService _paymentGateway;
|
||||
private readonly ILogger<PurchaseGoldenPackageCommandHandler> _logger;
|
||||
|
||||
public PurchaseGoldenPackageCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IPaymentGatewayService paymentGateway,
|
||||
ILogger<PurchaseGoldenPackageCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_paymentGateway = paymentGateway;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<PurchaseGoldenPackageResponseDto> Handle(PurchaseGoldenPackageCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Starting golden package purchase for UserId: {UserId}, PackageId: {PackageId}",
|
||||
request.UserId,
|
||||
request.PackageId);
|
||||
|
||||
// 1. پیدا کردن کاربر
|
||||
var user = await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("User not found for golden package purchase. UserId: {UserId}", request.UserId);
|
||||
throw new NotFoundException(nameof(User), request.UserId);
|
||||
}
|
||||
|
||||
// 2. جلوگیری از خرید مجدد پکیج طلایی
|
||||
if (user.PackagePurchaseMethod != PackagePurchaseMethod.None)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"User {UserId} has already purchased golden package via {Method}",
|
||||
request.UserId,
|
||||
user.PackagePurchaseMethod);
|
||||
|
||||
throw new ValidationException("شما قبلاً پکیج طلایی را خریداری کردهاید.");
|
||||
}
|
||||
|
||||
// 3. پیدا کردن پکیج
|
||||
var package = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.Id == request.PackageId, cancellationToken);
|
||||
|
||||
if (package == null)
|
||||
{
|
||||
_logger.LogWarning("Golden package not found. PackageId: {PackageId}", request.PackageId);
|
||||
throw new NotFoundException(nameof(Package), request.PackageId);
|
||||
}
|
||||
|
||||
// اطمینان از اینکه این همان پکیج طلایی است
|
||||
if (!package.Title.Contains("طلایی", StringComparison.OrdinalIgnoreCase) &&
|
||||
!package.Title.Contains("golden", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"PackageId {PackageId} is not a golden package. Title: {Title}",
|
||||
request.PackageId,
|
||||
package.Title);
|
||||
|
||||
throw new ValidationException("فقط پکیج طلایی قابل خرید است.");
|
||||
}
|
||||
|
||||
// 4. پیدا کردن آدرس پیشفرض کاربر (الزامی برای UserOrder)
|
||||
var defaultAddress = await _context.UserAddresses
|
||||
.Where(a => a.UserId == request.UserId)
|
||||
.OrderByDescending(a => a.Created)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (defaultAddress == null)
|
||||
{
|
||||
_logger.LogWarning("No address found for user {UserId} in golden package purchase", request.UserId);
|
||||
throw new ValidationException("لطفاً ابتدا یک آدرس برای خود ثبت کنید.");
|
||||
}
|
||||
|
||||
// 5. ایجاد سفارش
|
||||
var order = new UserOrder
|
||||
{
|
||||
UserId = user.Id,
|
||||
PackageId = package.Id,
|
||||
Amount = package.Price,
|
||||
PaymentStatus = PaymentStatus.Pending,
|
||||
DeliveryStatus = DeliveryStatus.None,
|
||||
UserAddressId = defaultAddress.Id,
|
||||
PaymentMethod = PaymentMethod.IPG
|
||||
};
|
||||
|
||||
_context.UserOrders.Add(order);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created golden package UserOrder {OrderId} for UserId {UserId}, Amount: {Amount}",
|
||||
order.Id,
|
||||
request.UserId,
|
||||
order.Amount);
|
||||
|
||||
// 6. شروع پرداخت با درگاه
|
||||
var paymentRequest = new PaymentRequest
|
||||
{
|
||||
Amount = order.Amount,
|
||||
UserId = user.Id,
|
||||
Mobile = user.Mobile ?? string.Empty,
|
||||
CallbackUrl = request.ReturnUrl,
|
||||
Description = $"خرید پکیج طلایی - سفارش #{order.Id}"
|
||||
};
|
||||
|
||||
var paymentResult = await _paymentGateway.InitiatePaymentAsync(paymentRequest, cancellationToken);
|
||||
|
||||
if (!paymentResult.IsSuccess)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Payment gateway initiation failed for golden package. OrderId {OrderId}: {ErrorMessage}",
|
||||
order.Id,
|
||||
paymentResult.ErrorMessage);
|
||||
|
||||
order.PaymentStatus = PaymentStatus.Reject;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
throw new Exception($"خطا در ارتباط با درگاه پرداخت: {paymentResult.ErrorMessage}");
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Golden package payment initiated successfully. OrderId: {OrderId}, RefId: {RefId}",
|
||||
order.Id,
|
||||
paymentResult.RefId);
|
||||
|
||||
return new PurchaseGoldenPackageResponseDto
|
||||
{
|
||||
Success = true,
|
||||
Message = "لطفاً به درگاه پرداخت منتقل شوید.",
|
||||
OrderId = order.Id,
|
||||
PaymentGatewayUrl = paymentResult.GatewayUrl ?? string.Empty,
|
||||
TrackingCode = paymentResult.RefId ?? string.Empty
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error in PurchaseGoldenPackageCommand for UserId: {UserId}",
|
||||
request.UserId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchaseGoldenPackage;
|
||||
|
||||
public class PurchaseGoldenPackageCommandValidator : AbstractValidator<PurchaseGoldenPackageCommand>
|
||||
{
|
||||
public PurchaseGoldenPackageCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.UserId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه کاربر باید بزرگتر از 0 باشد");
|
||||
|
||||
RuleFor(x => x.PackageId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه پکیج باید بزرگتر از 0 باشد");
|
||||
|
||||
RuleFor(x => x.ReturnUrl)
|
||||
.NotEmpty()
|
||||
.WithMessage("آدرس بازگشت الزامی است")
|
||||
.Must(url => Uri.TryCreate(url, UriKind.Absolute, out _))
|
||||
.WithMessage("آدرس بازگشت معتبر نیست");
|
||||
}
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchasePackage;
|
||||
|
||||
/// <summary>
|
||||
/// دستور خرید پکیج از طریق درگاه بانکی
|
||||
/// </summary>
|
||||
public class PurchasePackageCommand : IRequest<PaymentInitiateResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه کاربر
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
}
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ValidationException = FluentValidation.ValidationException;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchasePackage;
|
||||
|
||||
public class PurchasePackageCommandHandler
|
||||
: IRequestHandler<PurchasePackageCommand, PaymentInitiateResult>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IPaymentGatewayService _paymentGateway;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<PurchasePackageCommandHandler> _logger;
|
||||
|
||||
public PurchasePackageCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IPaymentGatewayService paymentGateway,
|
||||
IConfiguration configuration,
|
||||
ILogger<PurchasePackageCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_paymentGateway = paymentGateway;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<PaymentInitiateResult> Handle(
|
||||
PurchasePackageCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Starting package purchase for UserId: {UserId}",
|
||||
request.UserId
|
||||
);
|
||||
|
||||
// 1. بررسی وجود کاربر
|
||||
var user = await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("User not found: {UserId}", request.UserId);
|
||||
throw new NotFoundException(nameof(User), request.UserId);
|
||||
}
|
||||
|
||||
// 2. بررسی اینکه قبلاً پکیج نخریده باشد
|
||||
if (user.PackagePurchaseMethod != PackagePurchaseMethod.None)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"User {UserId} has already purchased package via {Method}",
|
||||
request.UserId,
|
||||
user.PackagePurchaseMethod
|
||||
);
|
||||
throw new ValidationException(
|
||||
"شما قبلاً پکیج را خریداری کردهاید"
|
||||
);
|
||||
}
|
||||
|
||||
// 3. پیدا کردن پکیج (فعلاً پکیج طلایی)
|
||||
var goldenPackage = await _context.Packages
|
||||
.FirstOrDefaultAsync(
|
||||
p => p.Title.Contains("طلایی") || p.Title.Contains("Golden"),
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (goldenPackage == null)
|
||||
{
|
||||
_logger.LogError("Package not found in database");
|
||||
throw new NotFoundException("پکیج یافت نشد");
|
||||
}
|
||||
|
||||
// 4. پیدا کردن آدرس پیشفرض کاربر (برای فیلد اجباری)
|
||||
var defaultAddress = await _context.UserAddresses
|
||||
.Where(a => a.UserId == request.UserId)
|
||||
.OrderByDescending(a => a.Created)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (defaultAddress == null)
|
||||
{
|
||||
_logger.LogWarning("No address found for user {UserId}", request.UserId);
|
||||
throw new ValidationException(
|
||||
"لطفاً ابتدا یک آدرس برای خود ثبت کنید"
|
||||
);
|
||||
}
|
||||
|
||||
// 5. ایجاد سفارش
|
||||
var order = new UserOrder
|
||||
{
|
||||
UserId = user.Id,
|
||||
PackageId = goldenPackage.Id,
|
||||
Amount = goldenPackage.Price, // 56,000,000 تومان
|
||||
PaymentStatus = PaymentStatus.Pending,
|
||||
DeliveryStatus = DeliveryStatus.None,
|
||||
UserAddressId = defaultAddress.Id,
|
||||
PaymentMethod = PaymentMethod.IPG
|
||||
};
|
||||
|
||||
_context.UserOrders.Add(order);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created UserOrder {OrderId} for UserId {UserId}, Amount: {Amount}",
|
||||
order.Id,
|
||||
request.UserId,
|
||||
order.Amount
|
||||
);
|
||||
|
||||
// 6. ایجاد درخواست پرداخت از درگاه
|
||||
var paymentRequest = new PaymentRequest
|
||||
{
|
||||
Amount = order.Amount,
|
||||
UserId = user.Id,
|
||||
Mobile = user.Mobile ?? "",
|
||||
CallbackUrl = $"{_configuration["CmsBaseUrl"] ?? "https://localhost:32846"}/api/package/verify-package",
|
||||
Description = $"خرید پکیج - سفارش #{order.Id}"
|
||||
};
|
||||
|
||||
var paymentResult = await _paymentGateway.InitiatePaymentAsync(paymentRequest);
|
||||
|
||||
if (!paymentResult.IsSuccess)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Payment gateway failed for OrderId {OrderId}: {ErrorMessage}",
|
||||
order.Id,
|
||||
paymentResult.ErrorMessage
|
||||
);
|
||||
|
||||
// بهروزرسانی وضعیت سفارش
|
||||
order.PaymentStatus = PaymentStatus.Reject;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
throw new Exception(
|
||||
$"خطا در ارتباط با درگاه پرداخت: {paymentResult.ErrorMessage}"
|
||||
);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Payment initiated successfully. OrderId: {OrderId}, RefId: {RefId}",
|
||||
order.Id,
|
||||
paymentResult.RefId
|
||||
);
|
||||
|
||||
return paymentResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error in PurchasePackageCommand for UserId: {UserId}",
|
||||
request.UserId
|
||||
);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.PurchasePackage;
|
||||
|
||||
public class PurchasePackageCommandValidator : AbstractValidator<PurchasePackageCommand>
|
||||
{
|
||||
public PurchasePackageCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.UserId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
|
||||
}
|
||||
}
|
||||
+14
-1
@@ -11,5 +11,18 @@ public record UpdatePackageCommand : IRequest<Unit>
|
||||
public string ImagePath { get; init; }
|
||||
//قیمت
|
||||
public long Price { get; init; }
|
||||
|
||||
// فیلدهای جدید پکیج
|
||||
public int SortOrder { get; init; }
|
||||
public bool IsActive { get; init; }
|
||||
public bool IsBasePackage { get; init; }
|
||||
public bool SupportsDayaPurchase { get; init; }
|
||||
public bool SupportsDirectPurchase { get; init; }
|
||||
public long ActivationFee { get; init; }
|
||||
public double DiscountMultiplier { get; init; }
|
||||
public double MagicWalletMultiplier { get; init; }
|
||||
public int MaxBalancesPerLeg { get; init; }
|
||||
public int MaxNetworkLevel { get; init; }
|
||||
public long MagicWalletMaxDeposit { get; init; }
|
||||
public long MagicWalletMaxCredit { get; init; }
|
||||
public List<long> FeatureIds { get; init; } = new();
|
||||
}
|
||||
+17
@@ -16,6 +16,23 @@ public class UpdatePackageCommandHandler : IRequestHandler<UpdatePackageCommand,
|
||||
request.Adapt(entity);
|
||||
_context.Packages.Update(entity);
|
||||
entity.AddDomainEvent(new UpdatePackageEvent(entity));
|
||||
|
||||
// Sync PackageFeatures — remove old, add new
|
||||
var existingFeatures = await _context.PackageFeatures
|
||||
.Where(pf => pf.PackageId == request.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
_context.PackageFeatures.RemoveRange(existingFeatures);
|
||||
|
||||
foreach (var featureId in request.FeatureIds)
|
||||
{
|
||||
_context.PackageFeatures.Add(new PackageFeature
|
||||
{
|
||||
PackageId = request.Id,
|
||||
ClubFeatureId = featureId,
|
||||
IsIncluded = true
|
||||
});
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return Unit.Value;
|
||||
}
|
||||
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyBasePackagePayment;
|
||||
|
||||
/// <summary>
|
||||
/// تکمیل یا رد پرداخت پکیج پایه (فراخوانی توسط BFF بعد از Callback)
|
||||
/// </summary>
|
||||
public record VerifyBasePackagePaymentCommand : IRequest<VerifyBasePackagePaymentResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه سفارش CMS
|
||||
/// </summary>
|
||||
public long OrderId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه تراکنش CMS
|
||||
/// </summary>
|
||||
public long TransactionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// آیا پرداخت موفق بوده؟ (نتیجه Verify از BFF)
|
||||
/// </summary>
|
||||
public bool PaymentSuccess { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// کد پیگیری بانکی (در صورت موفقیت)
|
||||
/// </summary>
|
||||
public string? RefId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// پیام از درگاه
|
||||
/// </summary>
|
||||
public string? Message { get; init; }
|
||||
}
|
||||
|
||||
public class VerifyBasePackagePaymentResponseDto
|
||||
{
|
||||
/// <summary>
|
||||
/// موفقیت عملیات
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// پیام
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// شناسه سفارش
|
||||
/// </summary>
|
||||
public long OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه تراکنش
|
||||
/// </summary>
|
||||
public long TransactionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// کد پیگیری بانکی
|
||||
/// </summary>
|
||||
public string? ReferenceCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// موجودی کیف پول بعد از شارژ
|
||||
/// </summary>
|
||||
public long WalletBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// موجودی تخفیف بعد از شارژ
|
||||
/// </summary>
|
||||
public long DiscountBalance { get; set; }
|
||||
}
|
||||
-205
@@ -1,205 +0,0 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using CMSMicroservice.Domain.Common;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyBasePackagePayment;
|
||||
|
||||
/// <summary>
|
||||
/// Handler برای تکمیل یا رد پرداخت پکیج پایه
|
||||
/// فراخوانی توسط BFF بعد از اینکه نتیجه Verify از PYMS مشخص شد
|
||||
///
|
||||
/// اگر PaymentSuccess = true:
|
||||
/// - شارژ کیف پول (Balance + DiscountBalance)
|
||||
/// - ثبت لاگ تغییر کیف پول
|
||||
/// - بهروزرسانی سفارش و کاربر
|
||||
///
|
||||
/// اگر PaymentSuccess = false:
|
||||
/// - فقط آپدیت وضعیت تراکنش و سفارش به Reject
|
||||
/// </summary>
|
||||
public class VerifyBasePackagePaymentCommandHandler : IRequestHandler<VerifyBasePackagePaymentCommand, VerifyBasePackagePaymentResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ILogger<VerifyBasePackagePaymentCommandHandler> _logger;
|
||||
|
||||
public VerifyBasePackagePaymentCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
ILogger<VerifyBasePackagePaymentCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<VerifyBasePackagePaymentResponseDto> Handle(
|
||||
VerifyBasePackagePaymentCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Processing base package payment result. OrderId: {OrderId}, TransactionId: {TransactionId}, PaymentSuccess: {PaymentSuccess}",
|
||||
request.OrderId,
|
||||
request.TransactionId,
|
||||
request.PaymentSuccess);
|
||||
|
||||
// 1. پیدا کردن سفارش با کاربر
|
||||
var order = await _context.UserOrders
|
||||
.Include(o => o.User)
|
||||
.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
|
||||
|
||||
if (order == null)
|
||||
{
|
||||
_logger.LogWarning("Order not found. OrderId: {OrderId}", request.OrderId);
|
||||
throw new NotFoundException(nameof(UserOrder), request.OrderId);
|
||||
}
|
||||
|
||||
// 2. پیدا کردن تراکنش
|
||||
var transaction = await _context.Transactions
|
||||
.FirstOrDefaultAsync(t => t.Id == request.TransactionId, cancellationToken);
|
||||
|
||||
if (transaction == null)
|
||||
{
|
||||
_logger.LogWarning("Transaction not found. TransactionId: {TransactionId}", request.TransactionId);
|
||||
throw new NotFoundException(nameof(Transaction), request.TransactionId);
|
||||
}
|
||||
|
||||
// 3. بررسی Idempotency - اگر قبلاً تایید شده باشد
|
||||
if (order.PaymentStatus == PaymentStatus.Success)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Order {OrderId} already verified successfully",
|
||||
request.OrderId);
|
||||
|
||||
var existingWallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == order.UserId, cancellationToken);
|
||||
|
||||
return new VerifyBasePackagePaymentResponseDto
|
||||
{
|
||||
Success = true,
|
||||
Message = "پرداخت قبلاً با موفقیت تایید شده است.",
|
||||
OrderId = order.Id,
|
||||
TransactionId = transaction.Id,
|
||||
ReferenceCode = transaction.RefId,
|
||||
WalletBalance = existingWallet?.Balance ?? 0,
|
||||
DiscountBalance = existingWallet?.DiscountBalance ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
// 4. اگر پرداخت ناموفق بود - فقط وضعیت را Reject میکنیم
|
||||
if (!request.PaymentSuccess)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Payment failed. OrderId: {OrderId}, Message: {Message}",
|
||||
request.OrderId,
|
||||
request.Message);
|
||||
|
||||
transaction.PaymentStatus = PaymentStatus.Reject;
|
||||
order.PaymentStatus = PaymentStatus.Reject;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new VerifyBasePackagePaymentResponseDto
|
||||
{
|
||||
Success = false,
|
||||
Message = request.Message ?? "پرداخت ناموفق بود.",
|
||||
OrderId = order.Id,
|
||||
TransactionId = transaction.Id
|
||||
};
|
||||
}
|
||||
|
||||
// 5. پرداخت موفق - پیدا کردن یا ایجاد کیف پول
|
||||
var userWallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == order.UserId, cancellationToken);
|
||||
|
||||
if (userWallet == null)
|
||||
{
|
||||
_logger.LogInformation("Creating new wallet for UserId: {UserId}", order.UserId);
|
||||
userWallet = new UserWallet
|
||||
{
|
||||
UserId = order.UserId,
|
||||
Balance = 0,
|
||||
DiscountBalance = 0,
|
||||
NetworkBalance = 0
|
||||
};
|
||||
_context.UserWallets.Add(userWallet);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// 6. شارژ کیف پول (هم Balance و هم DiscountBalance)
|
||||
var oldBalance = userWallet.Balance;
|
||||
var oldDiscountBalance = userWallet.DiscountBalance;
|
||||
|
||||
userWallet.Balance += SystemConstants.BasePackageAmount;
|
||||
userWallet.DiscountBalance += SystemConstants.BasePackageAmount * 2;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Charging wallet for user {UserId}. Balance: {OldBalance} -> {NewBalance}, DiscountBalance: {OldDiscount} -> {NewDiscount}",
|
||||
order.UserId,
|
||||
oldBalance,
|
||||
userWallet.Balance,
|
||||
oldDiscountBalance,
|
||||
userWallet.DiscountBalance);
|
||||
|
||||
// 7. بهروزرسانی Transaction
|
||||
transaction.PaymentStatus = PaymentStatus.Success;
|
||||
transaction.PaymentDate = DateTime.Now;
|
||||
transaction.RefId = request.RefId;
|
||||
|
||||
// 8. ثبت لاگ تغییر کیف پول
|
||||
var changeLog = new UserWalletChangeLog
|
||||
{
|
||||
WalletId = userWallet.Id,
|
||||
CurrentBalance = userWallet.Balance,
|
||||
ChangeValue = SystemConstants.BasePackageAmount,
|
||||
CurrentNetworkBalance = userWallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = userWallet.DiscountBalance,
|
||||
ChangeDiscountValue = SystemConstants.BasePackageAmount * 2,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
};
|
||||
|
||||
await _context.UserWalletChangeLogs.AddAsync(changeLog, cancellationToken);
|
||||
|
||||
// 9. بهروزرسانی Order
|
||||
order.TransactionId = transaction.Id;
|
||||
order.PaymentStatus = PaymentStatus.Success;
|
||||
order.PaymentDate = DateTime.Now;
|
||||
order.PaymentMethod = PaymentMethod.IPG;
|
||||
|
||||
// 10. بهروزرسانی User
|
||||
order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Base package payment completed successfully. OrderId: {OrderId}, UserId: {UserId}, TransactionId: {TransactionId}, RefId: {RefId}",
|
||||
order.Id,
|
||||
order.UserId,
|
||||
transaction.Id,
|
||||
request.RefId);
|
||||
|
||||
return new VerifyBasePackagePaymentResponseDto
|
||||
{
|
||||
Success = true,
|
||||
Message = "پرداخت با موفقیت تایید شد. کیف پول شما شارژ گردید.",
|
||||
OrderId = order.Id,
|
||||
TransactionId = transaction.Id,
|
||||
ReferenceCode = request.RefId,
|
||||
WalletBalance = userWallet.Balance,
|
||||
DiscountBalance = userWallet.DiscountBalance
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error in VerifyBasePackagePaymentCommand. OrderId: {OrderId}",
|
||||
request.OrderId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyBasePackagePayment;
|
||||
|
||||
public class VerifyBasePackagePaymentCommandValidator : AbstractValidator<VerifyBasePackagePaymentCommand>
|
||||
{
|
||||
public VerifyBasePackagePaymentCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.OrderId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه سفارش معتبر نیست.");
|
||||
|
||||
RuleFor(x => x.TransactionId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه تراکنش معتبر نیست.");
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
|
||||
|
||||
/// <summary>
|
||||
/// تایید پرداخت پکیج طلایی (پس از بازگشت از درگاه)
|
||||
/// </summary>
|
||||
public record VerifyGoldenPackagePurchaseCommand : IRequest<VerifyGoldenPackagePurchaseResponseDto>
|
||||
{
|
||||
public long OrderId { get; init; }
|
||||
public string Authority { get; init; } = string.Empty;
|
||||
public string Status { get; init; } = string.Empty; // OK یا NOK
|
||||
}
|
||||
|
||||
public class VerifyGoldenPackagePurchaseResponseDto
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public long OrderId { get; set; }
|
||||
public long TransactionId { get; set; }
|
||||
public string ReferenceCode { get; set; } = string.Empty;
|
||||
public long WalletBalance { get; set; }
|
||||
}
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
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;
|
||||
using ValidationException = FluentValidation.ValidationException;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
|
||||
|
||||
public class VerifyGoldenPackagePurchaseCommandHandler : IRequestHandler<VerifyGoldenPackagePurchaseCommand, VerifyGoldenPackagePurchaseResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IPaymentGatewayService _paymentGateway;
|
||||
private readonly ILogger<VerifyGoldenPackagePurchaseCommandHandler> _logger;
|
||||
|
||||
public VerifyGoldenPackagePurchaseCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IPaymentGatewayService paymentGateway,
|
||||
ILogger<VerifyGoldenPackagePurchaseCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_paymentGateway = paymentGateway;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<VerifyGoldenPackagePurchaseResponseDto> Handle(VerifyGoldenPackagePurchaseCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Verifying golden package purchase. OrderId: {OrderId}, Authority: {Authority}, Status: {Status}",
|
||||
request.OrderId,
|
||||
request.Authority,
|
||||
request.Status);
|
||||
|
||||
// 1. اگر پرداخت از سمت درگاه موفق گزارش نشده باشد
|
||||
if (!string.Equals(request.Status, "OK", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var pendingOrder = await _context.UserOrders
|
||||
.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
|
||||
|
||||
if (pendingOrder != null && pendingOrder.PaymentStatus == PaymentStatus.Pending)
|
||||
{
|
||||
pendingOrder.PaymentStatus = PaymentStatus.Reject;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
throw new ValidationException("پرداخت توسط کاربر لغو شد.");
|
||||
}
|
||||
|
||||
// 2. پیدا کردن سفارش به همراه کاربر
|
||||
var order = await _context.UserOrders
|
||||
.Include(o => o.User)
|
||||
.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
|
||||
|
||||
if (order == null)
|
||||
{
|
||||
_logger.LogWarning("Golden package order not found. OrderId: {OrderId}", request.OrderId);
|
||||
throw new NotFoundException(nameof(UserOrder), request.OrderId);
|
||||
}
|
||||
|
||||
// اگر قبلاً با موفقیت پرداخت شده، پاسخ idempotent برگردانیم
|
||||
if (order.PaymentStatus == PaymentStatus.Success && order.TransactionId.HasValue)
|
||||
{
|
||||
var existingWallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == order.UserId, cancellationToken);
|
||||
|
||||
var existingTransaction = await _context.Transactions
|
||||
.FirstOrDefaultAsync(t => t.Id == order.TransactionId.Value, cancellationToken);
|
||||
|
||||
return new VerifyGoldenPackagePurchaseResponseDto
|
||||
{
|
||||
Success = true,
|
||||
Message = "پرداخت قبلاً با موفقیت تایید شده است.",
|
||||
OrderId = order.Id,
|
||||
TransactionId = existingTransaction?.Id ?? order.TransactionId.Value,
|
||||
ReferenceCode = existingTransaction?.RefId ?? string.Empty,
|
||||
WalletBalance = existingWallet?.Balance ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Verify با درگاه پرداخت
|
||||
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
|
||||
request.Authority,
|
||||
request.Authority,
|
||||
cancellationToken);
|
||||
|
||||
if (!verifyResult.IsSuccess)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Golden package payment verification failed. OrderId: {OrderId}, Message: {Message}",
|
||||
request.OrderId,
|
||||
verifyResult.Message);
|
||||
|
||||
order.PaymentStatus = PaymentStatus.Reject;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
throw new ValidationException($"تراکنش ناموفق: {verifyResult.Message}");
|
||||
}
|
||||
|
||||
// 4. شارژ کیف پول (Balance فقط طبق سناریوی پکیج)
|
||||
var wallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == order.UserId, cancellationToken);
|
||||
|
||||
if (wallet == null)
|
||||
{
|
||||
_logger.LogError("Wallet not found for UserId: {UserId}", order.UserId);
|
||||
throw new NotFoundException($"کیف پول کاربر با شناسه {order.UserId} یافت نشد");
|
||||
}
|
||||
|
||||
var oldBalance = wallet.Balance;
|
||||
wallet.Balance += order.Amount;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Charging wallet Balance for user {UserId} from {OldBalance} to {NewBalance}",
|
||||
order.UserId,
|
||||
oldBalance,
|
||||
wallet.Balance);
|
||||
|
||||
// 5. ثبت Transaction
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Amount = order.Amount,
|
||||
Description = $"خرید پکیج طلایی از درگاه - سفارش #{order.Id}",
|
||||
PaymentStatus = PaymentStatus.Success,
|
||||
PaymentDate = DateTime.Now,
|
||||
RefId = verifyResult.RefId,
|
||||
Type = TransactionType.DepositIpg
|
||||
};
|
||||
|
||||
_context.Transactions.Add(transaction);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 6. ثبت لاگ تغییر کیف پول
|
||||
var changeLog = new UserWalletChangeLog
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
ChangeValue = order.Amount,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
ChangeDiscountValue = 0,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
};
|
||||
|
||||
await _context.UserWalletChangeLogs.AddAsync(changeLog, cancellationToken);
|
||||
|
||||
// 7. بهروزرسانی سفارش و کاربر
|
||||
order.TransactionId = transaction.Id;
|
||||
order.PaymentStatus = PaymentStatus.Success;
|
||||
order.PaymentDate = DateTime.Now;
|
||||
order.PaymentMethod = PaymentMethod.IPG;
|
||||
order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Golden package purchase verified successfully. OrderId: {OrderId}, UserId: {UserId}, TransactionId: {TransactionId}, RefId: {RefId}",
|
||||
order.Id,
|
||||
order.UserId,
|
||||
transaction.Id,
|
||||
verifyResult.RefId);
|
||||
|
||||
return new VerifyGoldenPackagePurchaseResponseDto
|
||||
{
|
||||
Success = true,
|
||||
Message = "پرداخت با موفقیت تایید شد. کیف پول شما شارژ گردید.",
|
||||
OrderId = order.Id,
|
||||
TransactionId = transaction.Id,
|
||||
ReferenceCode = verifyResult.RefId,
|
||||
WalletBalance = wallet.Balance
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error in VerifyGoldenPackagePurchaseCommand. OrderId: {OrderId}",
|
||||
request.OrderId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyGoldenPackagePurchase;
|
||||
|
||||
public class VerifyGoldenPackagePurchaseCommandValidator : AbstractValidator<VerifyGoldenPackagePurchaseCommand>
|
||||
{
|
||||
public VerifyGoldenPackagePurchaseCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.OrderId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه سفارش باید بزرگتر از 0 باشد");
|
||||
|
||||
RuleFor(x => x.Authority)
|
||||
.NotEmpty()
|
||||
.WithMessage("کد Authority الزامی است");
|
||||
|
||||
RuleFor(x => x.Status)
|
||||
.NotEmpty()
|
||||
.WithMessage("وضعیت پرداخت الزامی است")
|
||||
.Must(s => s == "OK" || s == "NOK")
|
||||
.WithMessage("وضعیت باید OK یا NOK باشد");
|
||||
}
|
||||
}
|
||||
+48
-14
@@ -58,10 +58,18 @@ public class VerifyPackagePurchaseCommandHandler
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. Verify با درگاه بانکی
|
||||
// واکشی PaymentTransaction برای گرفتن مبلغ (تومان) جهت verify
|
||||
var paymentTx = await _context.PaymentTransactions
|
||||
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, cancellationToken);
|
||||
|
||||
var amountInToman = (decimal)(paymentTx?.Amount ?? order.Amount);
|
||||
|
||||
// 3. Verify با درگاه بانکی (مبلغ به تومان — تبدیل به ریال در ZarinPalService)
|
||||
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
|
||||
request.Authority,
|
||||
request.Authority // verificationToken - در بعضی درگاهها همان Authority است
|
||||
"OK", // verificationToken — Status از درگاه
|
||||
amountInToman,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (!verifyResult.IsSuccess)
|
||||
@@ -99,9 +107,13 @@ public class VerifyPackagePurchaseCommandHandler
|
||||
wallet.Balance
|
||||
);
|
||||
|
||||
// شارژ DiscountBalance (موجودی تخفیف) — دو برابر مبلغ سفارش
|
||||
// شارژ DiscountBalance (موجودی تخفیف) — ضریب تخفیف از پکیج
|
||||
var oldDiscountBalance = wallet.DiscountBalance;
|
||||
wallet.DiscountBalance += order.Amount * 2;
|
||||
if (order.Package == null)
|
||||
throw new NotFoundException("پکیج مرتبط با سفارش یافت نشد");
|
||||
var discountMultiplier = order.Package.DiscountMultiplier;
|
||||
var discountAmount = (long)(order.Amount * discountMultiplier);
|
||||
wallet.DiscountBalance += discountAmount;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Charging DiscountBalance for UserId {UserId}: {OldBalance} -> {NewBalance}",
|
||||
@@ -125,22 +137,23 @@ public class VerifyPackagePurchaseCommandHandler
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 6. ثبت لاگ تغییر Balance
|
||||
var balanceLog = new UserWalletChangeLog
|
||||
var balanceLog = new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
ChangeValue = order.Amount,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance - (order.Amount * 2), // قبل از شارژ DiscountBalance
|
||||
CurrentDiscountBalance = wallet.DiscountBalance - discountAmount, // قبل از شارژ DiscountBalance
|
||||
ChangeDiscountValue = 0,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
RefrenceId = transaction.Id,
|
||||
PackageId = order.PackageId
|
||||
};
|
||||
await _context.UserWalletChangeLogs.AddAsync(balanceLog, cancellationToken);
|
||||
await _context.UserWalletHistories.AddAsync(balanceLog, cancellationToken);
|
||||
|
||||
// 7. ثبت لاگ تغییر DiscountBalance
|
||||
var discountLog = new UserWalletChangeLog
|
||||
var discountLog = new UserWalletHistory
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
@@ -148,19 +161,40 @@ public class VerifyPackagePurchaseCommandHandler
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
ChangeDiscountValue = order.Amount * 2,
|
||||
ChangeDiscountValue = discountAmount,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
RefrenceId = transaction.Id,
|
||||
PackageId = order.PackageId
|
||||
};
|
||||
await _context.UserWalletChangeLogs.AddAsync(discountLog, cancellationToken);
|
||||
await _context.UserWalletHistories.AddAsync(discountLog, cancellationToken);
|
||||
|
||||
// 8. بهروزرسانی Order
|
||||
// 8. ثبت UserPackagePurchase
|
||||
if (order.PackageId.HasValue)
|
||||
{
|
||||
var packagePurchase = new UserPackagePurchase
|
||||
{
|
||||
UserId = order.UserId,
|
||||
PackageId = order.PackageId.Value,
|
||||
PurchaseMethod = PackagePurchaseMethod.DirectPurchase,
|
||||
PurchasedAt = DateTime.Now,
|
||||
Amount = order.Amount,
|
||||
OrderId = order.Id,
|
||||
TransactionId = transaction.Id
|
||||
};
|
||||
await _context.UserPackagePurchases.AddAsync(packagePurchase, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created UserPackagePurchase for UserId {UserId}, PackageId {PackageId}",
|
||||
order.UserId, order.PackageId.Value);
|
||||
}
|
||||
|
||||
// 9. بهروزرسانی Order
|
||||
order.TransactionId = transaction.Id;
|
||||
order.PaymentStatus = PaymentStatus.Success;
|
||||
order.PaymentDate = DateTime.Now;
|
||||
order.PaymentMethod = PaymentMethod.IPG;
|
||||
|
||||
// 9. تغییر User.PackagePurchaseMethod
|
||||
// 10. تغییر User.PackagePurchaseMethod
|
||||
order.User.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
+17
-2
@@ -11,8 +11,10 @@ public class GetAllPackageByFilterQueryHandler : IRequestHandler<GetAllPackageBy
|
||||
public async Task<GetAllPackageByFilterResponseDto> Handle(GetAllPackageByFilterQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.Packages
|
||||
.Where(x => !x.IsDeleted)
|
||||
.ApplyOrder(sortBy: request.SortBy)
|
||||
.AsNoTracking()
|
||||
.Include(x => x.PackageFeatures)
|
||||
.AsQueryable();
|
||||
if (request.Filter is not null)
|
||||
{
|
||||
@@ -24,11 +26,24 @@ public class GetAllPackageByFilterQueryHandler : IRequestHandler<GetAllPackageBy
|
||||
.Where(x => request.Filter.Price == null || x.Price == request.Filter.Price)
|
||||
;
|
||||
}
|
||||
var packages = await query
|
||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var models = packages.Select(p =>
|
||||
{
|
||||
var model = p.Adapt<GetAllPackageByFilterResponseModel>();
|
||||
model.FeatureIds = p.PackageFeatures?
|
||||
.Where(pf => pf.IsIncluded)
|
||||
.Select(pf => pf.ClubFeatureId)
|
||||
.ToList() ?? new();
|
||||
return model;
|
||||
}).ToList();
|
||||
|
||||
return new GetAllPackageByFilterResponseDto
|
||||
{
|
||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.ProjectToType<GetAllPackageByFilterResponseModel>().ToListAsync(cancellationToken)
|
||||
Models = models
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -18,4 +18,18 @@ public class GetAllPackageByFilterResponseDto
|
||||
public string ImagePath { get; set; }
|
||||
//قیمت
|
||||
public long Price { get; set; }
|
||||
// فیلدهای جدید پکیج
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsBasePackage { get; set; }
|
||||
public bool SupportsDayaPurchase { get; set; }
|
||||
public bool SupportsDirectPurchase { get; set; }
|
||||
public long ActivationFee { get; set; }
|
||||
public decimal DiscountMultiplier { get; set; }
|
||||
public decimal MagicWalletMultiplier { get; set; }
|
||||
public int MaxBalancesPerLeg { get; set; }
|
||||
public int MaxNetworkLevel { get; set; }
|
||||
public long MagicWalletMaxDeposit { get; set; }
|
||||
public long MagicWalletMaxCredit { get; set; }
|
||||
public List<long> FeatureIds { get; set; } = new();
|
||||
}
|
||||
|
||||
+46
-30
@@ -18,44 +18,60 @@ public class GetCustomerPackageDetailsQueryHandler : IRequestHandler<GetCustomer
|
||||
{
|
||||
var package = await _context.Packages
|
||||
.AsNoTracking()
|
||||
.Where(x => x.Id == request.PackageId)
|
||||
.ProjectToType<GetCustomerPackageDetailsResponseDto>()
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
.Include(p => p.PackageFeatures)
|
||||
.ThenInclude(pf => pf.ClubFeature)
|
||||
.FirstOrDefaultAsync(x => x.Id == request.PackageId && !x.IsDeleted, cancellationToken);
|
||||
|
||||
if (package == null)
|
||||
throw new NotFoundException(nameof(Package), request.PackageId);
|
||||
|
||||
// Add features based on package (this could be stored in DB in future)
|
||||
package.Features = new List<PackageFeatureDto>
|
||||
var response = new GetCustomerPackageDetailsResponseDto
|
||||
{
|
||||
new PackageFeatureDto
|
||||
{
|
||||
Title = "درآمد کمیسیون",
|
||||
Description = "دریافت کمیسیون از فروش محصولات",
|
||||
Icon = "commission",
|
||||
IsHighlighted = true
|
||||
},
|
||||
new PackageFeatureDto
|
||||
{
|
||||
Title = "پشتیبانی 24/7",
|
||||
Description = "دسترسی به پشتیبانی در تمام ساعات شبانه روز",
|
||||
Icon = "support",
|
||||
IsHighlighted = false
|
||||
},
|
||||
new PackageFeatureDto
|
||||
{
|
||||
Title = "آموزشهای تخصصی",
|
||||
Description = "دسترسی به دورههای آموزشی و وبینارها",
|
||||
Icon = "education",
|
||||
IsHighlighted = true
|
||||
}
|
||||
Id = package.Id,
|
||||
Title = package.Title,
|
||||
Description = package.Description,
|
||||
Price = package.Price,
|
||||
ImagePath = package.ImagePath,
|
||||
ActivationFee = package.ActivationFee,
|
||||
DiscountMultiplier = (double)package.DiscountMultiplier,
|
||||
MagicWalletMultiplier = (double)package.MagicWalletMultiplier,
|
||||
MagicWalletMaxDeposit = package.MagicWalletMaxDeposit,
|
||||
MagicWalletMaxCredit = package.MagicWalletMaxCredit,
|
||||
IsBasePackage = package.IsBasePackage,
|
||||
SupportsDayaPurchase = package.SupportsDayaPurchase,
|
||||
SupportsDirectPurchase = package.SupportsDirectPurchase
|
||||
};
|
||||
|
||||
// Set purchase requirements
|
||||
package.Requirements = new PurchaseRequirementsDto
|
||||
// بارگذاری ویژگیها از DB (PackageFeatures → ClubFeature)
|
||||
if (package.PackageFeatures?.Any() == true)
|
||||
{
|
||||
response.Features = package.PackageFeatures
|
||||
.Where(pf => pf.ClubFeature != null)
|
||||
.Select(pf => new PackageFeatureDto
|
||||
{
|
||||
Title = pf.ClubFeature.Title ?? string.Empty,
|
||||
Description = pf.ClubFeature.Description ?? string.Empty,
|
||||
Icon = "feature",
|
||||
IsHighlighted = pf.ClubFeature.IsActive
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
// فالبک به ویژگیهای پیشفرض
|
||||
response.Features = new List<PackageFeatureDto>
|
||||
{
|
||||
new() { Title = "درآمد کمیسیون", Description = "دریافت کمیسیون از فروش محصولات", Icon = "commission", IsHighlighted = true },
|
||||
new() { Title = "پشتیبانی 24/7", Description = "دسترسی به پشتیبانی در تمام ساعات شبانه روز", Icon = "support", IsHighlighted = false },
|
||||
new() { Title = "آموزشهای تخصصی", Description = "دسترسی به دورههای آموزشی و وبینارها", Icon = "education", IsHighlighted = true }
|
||||
};
|
||||
}
|
||||
|
||||
// شرایط خرید
|
||||
response.Requirements = new PurchaseRequirementsDto
|
||||
{
|
||||
RequiresMembership = false,
|
||||
MinimumWalletBalance = package.Price / 10, // 10% minimum
|
||||
MinimumWalletBalance = package.Price / 10,
|
||||
Restrictions = new List<string>
|
||||
{
|
||||
"باید حداقل 18 سال سن داشته باشید",
|
||||
@@ -63,6 +79,6 @@ public class GetCustomerPackageDetailsQueryHandler : IRequestHandler<GetCustomer
|
||||
}
|
||||
};
|
||||
|
||||
return package;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -9,6 +9,15 @@ public class GetCustomerPackageDetailsResponseDto
|
||||
public string ImagePath { get; set; }
|
||||
public List<PackageFeatureDto> Features { get; set; } = new();
|
||||
public PurchaseRequirementsDto Requirements { get; set; }
|
||||
// New package-based fields
|
||||
public long ActivationFee { get; set; }
|
||||
public double DiscountMultiplier { get; set; }
|
||||
public double MagicWalletMultiplier { get; set; }
|
||||
public long MagicWalletMaxDeposit { get; set; }
|
||||
public long MagicWalletMaxCredit { get; set; }
|
||||
public bool IsBasePackage { get; set; }
|
||||
public bool SupportsDayaPurchase { get; set; }
|
||||
public bool SupportsDirectPurchase { get; set; }
|
||||
}
|
||||
|
||||
public class PackageFeatureDto
|
||||
|
||||
+35
-27
@@ -17,35 +17,43 @@ public class GetCustomerPackagesQueryHandler : IRequestHandler<GetCustomerPackag
|
||||
{
|
||||
var query = _context.Packages
|
||||
.AsNoTracking()
|
||||
.Where(p => !p.IsDeleted)
|
||||
.AsQueryable();
|
||||
|
||||
// Filter by PackageType if specified
|
||||
if (request.PackageTypeFilter.HasValue)
|
||||
// فیلتر IsActive
|
||||
if (!request.IncludeInactive)
|
||||
query = query.Where(p => p.IsActive);
|
||||
|
||||
// مرتبسازی بر اساس SortOrder
|
||||
query = query.OrderBy(p => p.SortOrder);
|
||||
|
||||
var packages = await query.ToListAsync(cancellationToken);
|
||||
|
||||
return packages.Select(p => new GetCustomerPackagesResponseDto
|
||||
{
|
||||
// Note: Package entity doesn't have PackageType enum, so we filter by convention
|
||||
// Assuming Title or Description contains the package type indicator
|
||||
// If Package entity needs PackageType field, it should be added to migration
|
||||
}
|
||||
|
||||
// Get all packages (assuming all are available unless marked otherwise)
|
||||
var packages = await query
|
||||
.ProjectToType<GetCustomerPackagesResponseDto>()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// Map additional fields
|
||||
foreach (var package in packages)
|
||||
{
|
||||
package.Name = package.Title;
|
||||
package.ImageUrl = package.ImagePath;
|
||||
package.Currency = "IRR";
|
||||
package.IsAvailable = true;
|
||||
package.ValidityDays = 365; // Default validity
|
||||
package.IsPopular = false;
|
||||
package.ShortDescription = package.Description?.Length > 100
|
||||
? package.Description.Substring(0, 100) + "..."
|
||||
: package.Description;
|
||||
}
|
||||
|
||||
return packages;
|
||||
Id = p.Id,
|
||||
Name = p.Title,
|
||||
Title = p.Title,
|
||||
Description = p.Description,
|
||||
Price = p.Price,
|
||||
ImageUrl = p.ImagePath,
|
||||
ImagePath = p.ImagePath,
|
||||
Currency = "IRR",
|
||||
IsAvailable = p.IsActive,
|
||||
ValidityDays = 365,
|
||||
IsPopular = p.IsBasePackage,
|
||||
ShortDescription = p.Description?.Length > 100
|
||||
? p.Description.Substring(0, 100) + "..."
|
||||
: p.Description,
|
||||
// New fields
|
||||
ActivationFee = p.ActivationFee,
|
||||
DiscountMultiplier = (double)p.DiscountMultiplier,
|
||||
MagicWalletMultiplier = (double)p.MagicWalletMultiplier,
|
||||
MagicWalletMaxDeposit = p.MagicWalletMaxDeposit,
|
||||
MagicWalletMaxCredit = p.MagicWalletMaxCredit,
|
||||
IsBasePackage = p.IsBasePackage,
|
||||
SupportsDayaPurchase = p.SupportsDayaPurchase,
|
||||
SupportsDirectPurchase = p.SupportsDirectPurchase
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -15,4 +15,13 @@ public class GetCustomerPackagesResponseDto
|
||||
public int ValidityDays { get; set; }
|
||||
public bool IsPopular { get; set; }
|
||||
public string ShortDescription { get; set; }
|
||||
// New package-based fields
|
||||
public long ActivationFee { get; set; }
|
||||
public double DiscountMultiplier { get; set; }
|
||||
public double MagicWalletMultiplier { get; set; }
|
||||
public long MagicWalletMaxDeposit { get; set; }
|
||||
public long MagicWalletMaxCredit { get; set; }
|
||||
public bool IsBasePackage { get; set; }
|
||||
public bool SupportsDayaPurchase { get; set; }
|
||||
public bool SupportsDirectPurchase { get; set; }
|
||||
}
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ public class GetCustomerPurchaseHistoryQueryHandler : IRequestHandler<GetCustome
|
||||
.AsNoTracking()
|
||||
.Where(x => x.UserId == userId && x.PackageId != null)
|
||||
.Include(x => x.Package)
|
||||
.Include(x => x.Transaction)
|
||||
.AsQueryable();
|
||||
|
||||
// Apply date filters if specified
|
||||
|
||||
+11
-4
@@ -11,12 +11,19 @@ public class GetPackageQueryHandler : IRequestHandler<GetPackageQuery, GetPackag
|
||||
public async Task<GetPackageResponseDto> Handle(GetPackageQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _context.Packages
|
||||
var package = await _context.Packages
|
||||
.AsNoTracking()
|
||||
.Include(x => x.PackageFeatures)
|
||||
.Where(x => x.Id == request.Id)
|
||||
.ProjectToType<GetPackageResponseDto>()
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
?? throw new NotFoundException(nameof(Package), request.Id);
|
||||
|
||||
return response ?? throw new NotFoundException(nameof(Package), request.Id);
|
||||
var response = package.Adapt<GetPackageResponseDto>();
|
||||
response.FeatureIds = package.PackageFeatures?
|
||||
.Where(pf => pf.IsIncluded)
|
||||
.Select(pf => pf.ClubFeatureId)
|
||||
.ToList() ?? new();
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-1
@@ -11,5 +11,18 @@ public class GetPackageResponseDto
|
||||
public string ImagePath { get; set; }
|
||||
//قیمت
|
||||
public long Price { get; set; }
|
||||
|
||||
// فیلدهای جدید پکیج
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsBasePackage { get; set; }
|
||||
public bool SupportsDayaPurchase { get; set; }
|
||||
public bool SupportsDirectPurchase { get; set; }
|
||||
public long ActivationFee { get; set; }
|
||||
public decimal DiscountMultiplier { get; set; }
|
||||
public decimal MagicWalletMultiplier { get; set; }
|
||||
public int MaxBalancesPerLeg { get; set; }
|
||||
public int MaxNetworkLevel { get; set; }
|
||||
public long MagicWalletMaxDeposit { get; set; }
|
||||
public long MagicWalletMaxCredit { get; set; }
|
||||
public List<long> FeatureIds { get; set; } = new();
|
||||
}
|
||||
+44
-41
@@ -1,3 +1,4 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
@@ -15,47 +16,49 @@ public class GetUserPackageStatusQueryHandler : IRequestHandler<GetUserPackageSt
|
||||
|
||||
public async Task<UserPackageStatusDto> Handle(GetUserPackageStatusQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: پیادهسازی دریافت وضعیت پکیج کاربر
|
||||
//
|
||||
// 1. دریافت اطلاعات کاربر:
|
||||
// - var user = await _context.Users
|
||||
// .Include(u => u.UserWallet)
|
||||
// .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken)
|
||||
// - if (user == null) throw new NotFoundException("کاربر یافت نشد")
|
||||
//
|
||||
// 2. دریافت عضویت باشگاه:
|
||||
// - var clubMembership = await _context.ClubMemberships
|
||||
// .FirstOrDefaultAsync(c => c.UserId == user.Id && c.IsActive, cancellationToken)
|
||||
//
|
||||
// 3. دریافت آخرین سفارش پکیج:
|
||||
// - var lastPackageOrder = await _context.UserOrders
|
||||
// .Where(o => o.UserId == user.Id && o.PackageId != null)
|
||||
// .OrderByDescending(o => o.Created)
|
||||
// .FirstOrDefaultAsync(cancellationToken)
|
||||
//
|
||||
// 4. بررسی شرایط فعالسازی باشگاه:
|
||||
// - var wallet = user.UserWallet
|
||||
// - bool canActivate =
|
||||
// user.PackagePurchaseMethod != PackagePurchaseMethod.None &&
|
||||
// clubMembership == null &&
|
||||
// wallet != null &&
|
||||
// wallet.Balance >= 56_000_000
|
||||
//
|
||||
// 5. برگشت DTO:
|
||||
// - return new UserPackageStatusDto {
|
||||
// UserId = user.Id,
|
||||
// PackagePurchaseMethod = user.PackagePurchaseMethod.ToString(),
|
||||
// HasPurchasedPackage = user.PackagePurchaseMethod != PackagePurchaseMethod.None,
|
||||
// IsClubMemberActive = clubMembership != null,
|
||||
// WalletBalance = wallet?.Balance ?? 0,
|
||||
// DiscountBalance = wallet?.DiscountBalance ?? 0,
|
||||
// CanActivateClubMembership = canActivate,
|
||||
// LastOrderNumber = lastPackageOrder?.OrderNumber,
|
||||
// LastPurchaseDate = lastPackageOrder?.Created
|
||||
// }
|
||||
//
|
||||
// نکته: این query برای UI مفید است تا وضعیت کاربر را نمایش دهد
|
||||
// 1. دریافت اطلاعات کاربر
|
||||
var user = await _context.Users
|
||||
.Include(u => u.UserWallets)
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
|
||||
throw new NotImplementedException("GetUserPackageStatus needs implementation");
|
||||
if (user == null)
|
||||
throw new NotFoundException(nameof(User), request.UserId);
|
||||
|
||||
// 2. دریافت عضویت باشگاه
|
||||
var clubMembership = await _context.ClubMemberships
|
||||
.FirstOrDefaultAsync(c => c.UserId == user.Id && c.IsActive, cancellationToken);
|
||||
|
||||
// 3. دریافت آخرین سفارش پکیج
|
||||
var lastPackageOrder = await _context.UserOrders
|
||||
.Where(o => o.UserId == user.Id && o.PackageId != null)
|
||||
.OrderByDescending(o => o.Created)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
// 4. بارگذاری پکیج پایه برای بررسی شرایط فعالسازی
|
||||
var basePackage = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken);
|
||||
|
||||
var wallet = user.UserWallets.FirstOrDefault();
|
||||
|
||||
// 5. بررسی شرایط فعالسازی باشگاه
|
||||
var canActivate =
|
||||
user.PackagePurchaseMethod != PackagePurchaseMethod.None &&
|
||||
clubMembership == null &&
|
||||
wallet != null &&
|
||||
basePackage != null &&
|
||||
wallet.Balance >= basePackage.Price;
|
||||
|
||||
return new UserPackageStatusDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
PackagePurchaseMethod = user.PackagePurchaseMethod.ToString(),
|
||||
HasPurchasedPackage = user.PackagePurchaseMethod != PackagePurchaseMethod.None,
|
||||
IsClubMemberActive = clubMembership?.IsActive ?? false,
|
||||
WalletBalance = wallet?.Balance ?? 0,
|
||||
DiscountBalance = wallet?.DiscountBalance ?? 0,
|
||||
CanActivateClubMembership = canActivate,
|
||||
LastOrderNumber = lastPackageOrder?.Id.ToString(),
|
||||
LastPurchaseDate = lastPackageOrder?.Created
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -72,11 +72,10 @@ public class CreateNewOtpTokenCommandHandler : IRequestHandler<CreateNewOtpToken
|
||||
|
||||
try
|
||||
{
|
||||
// برای امضای قرارداد، شناسه GUID هم در پیامک ارسال شود
|
||||
// برای امضای قرارداد، از الگوی Sign-Contract با GUID به عنوان token2 استفاده شود
|
||||
if ((purpose == "signcontract" || purpose == "signclubcontract") && !string.IsNullOrEmpty(request.SignGuid))
|
||||
{
|
||||
var message = $"کد تایید امضای قرارداد: {code}\nشناسه قرارداد: {request.SignGuid}";
|
||||
await _kavenegarService.SendAsync(mobile, message);
|
||||
await _kavenegarService.VerifyLookupAsync(mobile, code, request.SignGuid, "Sign-Contract");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ public class GetCustomerReferralsQueryHandler : IRequestHandler<GetCustomerRefer
|
||||
|
||||
// Calculate this month's commission from wallet changelog
|
||||
var startOfMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
|
||||
var thisMonthCommission = await _context.UserWalletChangeLogs
|
||||
var thisMonthCommission = await _context.UserWalletHistories
|
||||
.AsNoTracking()
|
||||
.Include(x => x.Wallet)
|
||||
.Where(x => x.Wallet.UserId == userId && x.Created >= startOfMonth)
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletChangeLog;
|
||||
namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory;
|
||||
|
||||
public class GetCustomerWalletChangeLogQuery : IRequest<List<GetCustomerWalletChangeLogResponseDto>>
|
||||
public class GetCustomerWalletHistoryQuery : IRequest<List<GetCustomerWalletHistoryResponseDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// فیلتر بر اساس شناسه ارجاع (اختیاری)
|
||||
+7
-7
@@ -1,11 +1,11 @@
|
||||
namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletChangeLog;
|
||||
namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory;
|
||||
|
||||
public class GetCustomerWalletChangeLogQueryHandler : IRequestHandler<GetCustomerWalletChangeLogQuery, List<GetCustomerWalletChangeLogResponseDto>>
|
||||
public class GetCustomerWalletHistoryQueryHandler : IRequestHandler<GetCustomerWalletHistoryQuery, List<GetCustomerWalletHistoryResponseDto>>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public GetCustomerWalletChangeLogQueryHandler(
|
||||
public GetCustomerWalletHistoryQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
ICurrentUserService currentUser)
|
||||
{
|
||||
@@ -13,8 +13,8 @@ public class GetCustomerWalletChangeLogQueryHandler : IRequestHandler<GetCustome
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<List<GetCustomerWalletChangeLogResponseDto>> Handle(
|
||||
GetCustomerWalletChangeLogQuery request,
|
||||
public async Task<List<GetCustomerWalletHistoryResponseDto>> Handle(
|
||||
GetCustomerWalletHistoryQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Get current user's ID from JWT
|
||||
@@ -35,7 +35,7 @@ public class GetCustomerWalletChangeLogQueryHandler : IRequestHandler<GetCustome
|
||||
}
|
||||
|
||||
// Build query for wallet change logs
|
||||
var query = _context.UserWalletChangeLogs
|
||||
var query = _context.UserWalletHistories
|
||||
.AsNoTracking()
|
||||
.Where(x => x.WalletId == userWallet.Id);
|
||||
|
||||
@@ -53,7 +53,7 @@ public class GetCustomerWalletChangeLogQueryHandler : IRequestHandler<GetCustome
|
||||
// Order by newest first
|
||||
var result = await query
|
||||
.OrderByDescending(x => x.Created)
|
||||
.ProjectToType<GetCustomerWalletChangeLogResponseDto>()
|
||||
.ProjectToType<GetCustomerWalletHistoryResponseDto>()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return result;
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletChangeLog;
|
||||
namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletHistory;
|
||||
|
||||
public class GetCustomerWalletChangeLogResponseDto
|
||||
public class GetCustomerWalletHistoryResponseDto
|
||||
{
|
||||
/// <summary>
|
||||
/// موجودی جاری
|
||||
+1
-1
@@ -3,7 +3,7 @@ namespace CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawal
|
||||
public class GetCustomerWithdrawalSettingsResponseDto
|
||||
{
|
||||
/// <summary>
|
||||
/// حداقل مبلغ برداشت (ریال)
|
||||
/// حداقل مبلغ برداشت (تومان)
|
||||
/// </summary>
|
||||
public long MinWithdrawalAmount { get; set; }
|
||||
}
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
using CMSMicroservice.Domain.Events;
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.CreateNewUserWalletChangeLog;
|
||||
public class CreateNewUserWalletChangeLogCommandHandler : IRequestHandler<CreateNewUserWalletChangeLogCommand, CreateNewUserWalletChangeLogResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public CreateNewUserWalletChangeLogCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<CreateNewUserWalletChangeLogResponseDto> Handle(CreateNewUserWalletChangeLogCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = request.Adapt<UserWalletChangeLog>();
|
||||
await _context.UserWalletChangeLogs.AddAsync(entity, cancellationToken);
|
||||
entity.AddDomainEvent(new CreateNewUserWalletChangeLogEvent(entity));
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return entity.Adapt<CreateNewUserWalletChangeLogResponseDto>();
|
||||
}
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.CreateNewUserWalletChangeLog;
|
||||
public class CreateNewUserWalletChangeLogResponseDto
|
||||
{
|
||||
//شناسه
|
||||
public long Id { get; set; }
|
||||
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.DeleteUserWalletChangeLog;
|
||||
public record DeleteUserWalletChangeLogCommand : IRequest<Unit>
|
||||
{
|
||||
//شناسه
|
||||
public long Id { get; init; }
|
||||
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
using CMSMicroservice.Domain.Events;
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.DeleteUserWalletChangeLog;
|
||||
public class DeleteUserWalletChangeLogCommandHandler : IRequestHandler<DeleteUserWalletChangeLogCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public DeleteUserWalletChangeLogCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(DeleteUserWalletChangeLogCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.UserWalletChangeLogs
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(UserWalletChangeLog), request.Id);
|
||||
entity.IsDeleted = true;
|
||||
_context.UserWalletChangeLogs.Update(entity);
|
||||
entity.AddDomainEvent(new DeleteUserWalletChangeLogEvent(entity));
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.DeleteUserWalletChangeLog;
|
||||
public class DeleteUserWalletChangeLogCommandValidator : AbstractValidator<DeleteUserWalletChangeLogCommand>
|
||||
{
|
||||
public DeleteUserWalletChangeLogCommandValidator()
|
||||
{
|
||||
RuleFor(model => model.Id)
|
||||
.NotNull();
|
||||
}
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
{
|
||||
var result = await ValidateAsync(ValidationContext<DeleteUserWalletChangeLogCommand>.CreateWithOptions((DeleteUserWalletChangeLogCommand)model, x => x.IncludeProperties(propertyName)));
|
||||
if (result.IsValid)
|
||||
return Array.Empty<string>();
|
||||
return result.Errors.Select(e => e.ErrorMessage);
|
||||
};
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
using CMSMicroservice.Domain.Events;
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.UpdateUserWalletChangeLog;
|
||||
public class UpdateUserWalletChangeLogCommandHandler : IRequestHandler<UpdateUserWalletChangeLogCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public UpdateUserWalletChangeLogCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(UpdateUserWalletChangeLogCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.UserWalletChangeLogs
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(UserWalletChangeLog), request.Id);
|
||||
request.Adapt(entity);
|
||||
_context.UserWalletChangeLogs.Update(entity);
|
||||
entity.AddDomainEvent(new UpdateUserWalletChangeLogEvent(entity));
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
using CMSMicroservice.Domain.Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.EventHandlers;
|
||||
|
||||
public class CreateNewUserWalletChangeLogEventHandler : INotificationHandler<CreateNewUserWalletChangeLogEvent>
|
||||
{
|
||||
private readonly ILogger<
|
||||
CreateNewUserWalletChangeLogEventHandler> _logger;
|
||||
|
||||
public CreateNewUserWalletChangeLogEventHandler(ILogger<CreateNewUserWalletChangeLogEventHandler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task Handle(CreateNewUserWalletChangeLogEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().Name);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
using CMSMicroservice.Domain.Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.EventHandlers;
|
||||
|
||||
public class DeleteUserWalletChangeLogEventHandler : INotificationHandler<DeleteUserWalletChangeLogEvent>
|
||||
{
|
||||
private readonly ILogger<
|
||||
DeleteUserWalletChangeLogEventHandler> _logger;
|
||||
|
||||
public DeleteUserWalletChangeLogEventHandler(ILogger<DeleteUserWalletChangeLogEventHandler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task Handle(DeleteUserWalletChangeLogEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().Name);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
using CMSMicroservice.Domain.Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.EventHandlers;
|
||||
|
||||
public class UpdateUserWalletChangeLogEventHandler : INotificationHandler<UpdateUserWalletChangeLogEvent>
|
||||
{
|
||||
private readonly ILogger<
|
||||
UpdateUserWalletChangeLogEventHandler> _logger;
|
||||
|
||||
public UpdateUserWalletChangeLogEventHandler(ILogger<UpdateUserWalletChangeLogEventHandler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task Handle(UpdateUserWalletChangeLogEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().Name);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Queries.GetAllUserWalletChangeLogByFilter;
|
||||
public class GetAllUserWalletChangeLogByFilterQueryValidator : AbstractValidator<GetAllUserWalletChangeLogByFilterQuery>
|
||||
{
|
||||
public GetAllUserWalletChangeLogByFilterQueryValidator()
|
||||
{
|
||||
}
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
{
|
||||
var result = await ValidateAsync(ValidationContext<GetAllUserWalletChangeLogByFilterQuery>.CreateWithOptions((GetAllUserWalletChangeLogByFilterQuery)model, x => x.IncludeProperties(propertyName)));
|
||||
if (result.IsValid)
|
||||
return Array.Empty<string>();
|
||||
return result.Errors.Select(e => e.ErrorMessage);
|
||||
};
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Queries.GetUserWalletChangeLog;
|
||||
public record GetUserWalletChangeLogQuery : IRequest<GetUserWalletChangeLogResponseDto>
|
||||
{
|
||||
//شناسه
|
||||
public long Id { get; init; }
|
||||
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Queries.GetUserWalletChangeLog;
|
||||
public class GetUserWalletChangeLogQueryHandler : IRequestHandler<GetUserWalletChangeLogQuery, GetUserWalletChangeLogResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetUserWalletChangeLogQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetUserWalletChangeLogResponseDto> Handle(GetUserWalletChangeLogQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _context.UserWalletChangeLogs
|
||||
.AsNoTracking()
|
||||
.Where(x => x.Id == request.Id)
|
||||
.ProjectToType<GetUserWalletChangeLogResponseDto>()
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return response ?? throw new NotFoundException(nameof(UserWalletChangeLog), request.Id);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.CreateNewUserWalletChangeLog;
|
||||
public record CreateNewUserWalletChangeLogCommand : IRequest<CreateNewUserWalletChangeLogResponseDto>
|
||||
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.CreateNewUserWalletHistory;
|
||||
public record CreateNewUserWalletHistoryCommand : IRequest<CreateNewUserWalletHistoryResponseDto>
|
||||
{
|
||||
//شناسه کیف پول
|
||||
public long WalletId { get; init; }
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
using CMSMicroservice.Domain.Events;
|
||||
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.CreateNewUserWalletHistory;
|
||||
public class CreateNewUserWalletHistoryCommandHandler : IRequestHandler<CreateNewUserWalletHistoryCommand, CreateNewUserWalletHistoryResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public CreateNewUserWalletHistoryCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<CreateNewUserWalletHistoryResponseDto> Handle(CreateNewUserWalletHistoryCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = request.Adapt<UserWalletHistory>();
|
||||
await _context.UserWalletHistories.AddAsync(entity, cancellationToken);
|
||||
entity.AddDomainEvent(new CreateNewUserWalletHistoryEvent(entity));
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return entity.Adapt<CreateNewUserWalletHistoryResponseDto>();
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.CreateNewUserWalletChangeLog;
|
||||
public class CreateNewUserWalletChangeLogCommandValidator : AbstractValidator<CreateNewUserWalletChangeLogCommand>
|
||||
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.CreateNewUserWalletHistory;
|
||||
public class CreateNewUserWalletHistoryCommandValidator : AbstractValidator<CreateNewUserWalletHistoryCommand>
|
||||
{
|
||||
public CreateNewUserWalletChangeLogCommandValidator()
|
||||
public CreateNewUserWalletHistoryCommandValidator()
|
||||
{
|
||||
RuleFor(model => model.WalletId)
|
||||
.NotNull();
|
||||
@@ -18,7 +18,7 @@ public class CreateNewUserWalletChangeLogCommandValidator : AbstractValidator<Cr
|
||||
}
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
{
|
||||
var result = await ValidateAsync(ValidationContext<CreateNewUserWalletChangeLogCommand>.CreateWithOptions((CreateNewUserWalletChangeLogCommand)model, x => x.IncludeProperties(propertyName)));
|
||||
var result = await ValidateAsync(ValidationContext<CreateNewUserWalletHistoryCommand>.CreateWithOptions((CreateNewUserWalletHistoryCommand)model, x => x.IncludeProperties(propertyName)));
|
||||
if (result.IsValid)
|
||||
return Array.Empty<string>();
|
||||
return result.Errors.Select(e => e.ErrorMessage);
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.CreateNewUserWalletHistory;
|
||||
public class CreateNewUserWalletHistoryResponseDto
|
||||
{
|
||||
//شناسه
|
||||
public long Id { get; set; }
|
||||
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.DeleteUserWalletHistory;
|
||||
public record DeleteUserWalletHistoryCommand : IRequest<Unit>
|
||||
{
|
||||
//شناسه
|
||||
public long Id { get; init; }
|
||||
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using CMSMicroservice.Domain.Events;
|
||||
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.DeleteUserWalletHistory;
|
||||
public class DeleteUserWalletHistoryCommandHandler : IRequestHandler<DeleteUserWalletHistoryCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public DeleteUserWalletHistoryCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(DeleteUserWalletHistoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.UserWalletHistories
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(UserWalletHistory), request.Id);
|
||||
entity.IsDeleted = true;
|
||||
_context.UserWalletHistories.Update(entity);
|
||||
entity.AddDomainEvent(new DeleteUserWalletHistoryEvent(entity));
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.DeleteUserWalletHistory;
|
||||
public class DeleteUserWalletHistoryCommandValidator : AbstractValidator<DeleteUserWalletHistoryCommand>
|
||||
{
|
||||
public DeleteUserWalletHistoryCommandValidator()
|
||||
{
|
||||
RuleFor(model => model.Id)
|
||||
.NotNull();
|
||||
}
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
{
|
||||
var result = await ValidateAsync(ValidationContext<DeleteUserWalletHistoryCommand>.CreateWithOptions((DeleteUserWalletHistoryCommand)model, x => x.IncludeProperties(propertyName)));
|
||||
if (result.IsValid)
|
||||
return Array.Empty<string>();
|
||||
return result.Errors.Select(e => e.ErrorMessage);
|
||||
};
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
namespace CMSMicroservice.Application.UserWalletChangeLogCQ.Commands.UpdateUserWalletChangeLog;
|
||||
public record UpdateUserWalletChangeLogCommand : IRequest<Unit>
|
||||
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.UpdateUserWalletHistory;
|
||||
public record UpdateUserWalletHistoryCommand : IRequest<Unit>
|
||||
{
|
||||
//شناسه
|
||||
public long Id { get; init; }
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using CMSMicroservice.Domain.Events;
|
||||
namespace CMSMicroservice.Application.UserWalletHistoryCQ.Commands.UpdateUserWalletHistory;
|
||||
public class UpdateUserWalletHistoryCommandHandler : IRequestHandler<UpdateUserWalletHistoryCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public UpdateUserWalletHistoryCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(UpdateUserWalletHistoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.UserWalletHistories
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(UserWalletHistory), request.Id);
|
||||
request.Adapt(entity);
|
||||
_context.UserWalletHistories.Update(entity);
|
||||
entity.AddDomainEvent(new UpdateUserWalletHistoryEvent(entity));
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user