Files
CMS/src/CMSMicroservice.WebApi/Services/PackageService.cs
T
masoodafar-web e5bc3a952a
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m33s
feat: ClubMembershipCycle IHasHistory + PackageId in WalletHistory (F1)
- ClubMembershipCycle now implements IHasHistory<ClubMembershipCycleHistory>
  - CreateHistorySnapshot: New* fields from current state, Old* auto-filled by interceptor
  - Parses action string to ClubMembershipCycleAction enum

- Add PackageId to 6 UserWalletHistory creation sites (F1 fix):
  - VerifyPackagePurchaseCommandHandler: balanceLog + discountLog (order.PackageId)
  - CheckAndProcessDayaLoansCommandHandler: mainLog + discountLog (package.Id)
  - CreateManualPaymentCommandHandler: walletLog (package.Id)
  - PackageService.CustomerVerifyPackagePurchase: walletLog (purchase.PackageId)
  - Non-package flows (orders, commissions, manual) correctly keep PackageId=null
2026-02-27 07:07:30 +03:30

417 lines
18 KiB
C#

using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.PackageCQ.Commands.CreateNewPackage;
using CMSMicroservice.Application.PackageCQ.Commands.UpdatePackage;
using CMSMicroservice.Application.PackageCQ.Commands.DeletePackage;
using CMSMicroservice.Application.PackageCQ.Queries.GetPackage;
using CMSMicroservice.Application.PackageCQ.Queries.GetAllPackageByFilter;
using CMSMicroservice.Application.PackageCQ.Queries.GetUserPackageStatus;
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackages;
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackageDetails;
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPurchaseHistory;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Payment;
using AppModels = CMSMicroservice.Application.Common.Models;
using Grpc.Core;
using Google.Protobuf.WellKnownTypes;
using System.Collections.Generic;
using System.Linq;
using CMSMicroservice.Protobuf.Protos;
using Microsoft.EntityFrameworkCore;
using MediatR;
using Mapster;
using Microsoft.Extensions.Configuration;
namespace CMSMicroservice.WebApi.Services;
public class PackageService : PackageContract.PackageContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
private readonly IPaymentGatewayService _paymentGateway;
private readonly IConfiguration _configuration;
public PackageService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
IApplicationDbContext context,
ICurrentUserService currentUserService,
IPaymentGatewayService paymentGateway,
IConfiguration configuration)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
_currentUserService = currentUserService;
_paymentGateway = paymentGateway;
_configuration = configuration;
}
public override async Task<CreateNewPackageResponse> CreateNewPackage(CreateNewPackageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateNewPackageRequest, CreateNewPackageCommand, CreateNewPackageResponse>(request, context);
}
public override async Task<Empty> UpdatePackage(UpdatePackageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdatePackageRequest, UpdatePackageCommand>(request, context);
}
public override async Task<Empty> DeletePackage(DeletePackageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeletePackageRequest, DeletePackageCommand>(request, context);
}
public override async Task<GetPackageResponse> GetPackage(GetPackageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetPackageRequest, GetPackageQuery, GetPackageResponse>(request, context);
}
public override async Task<GetAllPackageByFilterResponse> GetAllPackageByFilter(GetAllPackageByFilterRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllPackageByFilterRequest, GetAllPackageByFilterQuery, GetAllPackageByFilterResponse>(request, context);
}
public override async Task<GetUserPackageStatusResponse> GetUserPackageStatus(GetUserPackageStatusRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserPackageStatusRequest, GetUserPackageStatusQuery, GetUserPackageStatusResponse>(request, context);
}
// ============= Customer-specific Method Implementations =============
public override async Task<GetCustomerPackagesResponse> GetCustomerPackages(GetCustomerPackagesRequest request, ServerCallContext context)
{
var query = new GetCustomerPackagesQuery
{
IncludeInactive = request.IncludeInactive,
PackageTypeFilter = request.PackageTypeFilter != PackageTypeEnum.PackageTypeBasic
? (int?)request.PackageTypeFilter
: null
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerPackagesResponse();
response.Models.AddRange(result.Adapt<List<CustomerPackageModel>>());
return response;
}
public override async Task<GetCustomerPackageDetailsResponse> GetCustomerPackageDetails(GetCustomerPackageDetailsRequest request, ServerCallContext context)
{
var query = new GetCustomerPackageDetailsQuery
{
PackageId = request.PackageId
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerPackageDetailsResponse
{
Id = result.Id,
Title = result.Title,
Description = result.Description,
Price = result.Price,
ImagePath = result.ImagePath
};
// Map Features
foreach (var feature in result.Features)
{
response.Features.Add(new PackageFeature
{
Title = feature.Title,
Description = feature.Description,
Icon = feature.Icon,
IsHighlighted = feature.IsHighlighted
});
}
// Map Requirements
response.Requirements = new PurchaseRequirements
{
RequiresMembership = result.Requirements.RequiresMembership,
MinimumWalletBalance = result.Requirements.MinimumWalletBalance
};
response.Requirements.Restrictions.AddRange(result.Requirements.Restrictions);
return response;
}
public override async Task<CustomerPurchasePackageResponse> CustomerPurchasePackage(CustomerPurchasePackageRequest request, ServerCallContext context)
{
var userId = GetCurrentUserId();
// Lookup package
var package = await _context.Packages
.AsNoTracking()
.Where(p => p.Id == request.PackageId && !p.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (package == null)
throw new RpcException(new Status(StatusCode.NotFound, "پکیج مورد نظر یافت نشد"));
// Create transaction
var transaction = new CMSMicroservice.Domain.Entities.Transaction
{
Amount = package.Price,
Description = $"خرید پکیج {package.Title}",
PaymentStatus = Domain.Enums.PaymentStatus.Pending,
Type = Domain.Enums.TransactionType.Buy
};
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(context.CancellationToken);
// Create purchase record
var purchaseMethod = request.PurchaseMethod == PurchaseMethodEnum.PurchaseMethodGateway
? Domain.Enums.PackagePurchaseMethod.DirectPurchase
: Domain.Enums.PackagePurchaseMethod.DayaLoan;
var purchase = new CMSMicroservice.Domain.Entities.UserPackagePurchase
{
UserId = userId,
PackageId = package.Id,
PurchaseMethod = purchaseMethod,
PurchasedAt = DateTime.UtcNow,
Amount = package.Price,
TransactionId = transaction.Id
};
_context.UserPackagePurchases.Add(purchase);
await _context.SaveChangesAsync(context.CancellationToken);
// Initiate payment with gateway
var user = await _context.Users
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u => new { u.Mobile })
.FirstOrDefaultAsync(context.CancellationToken);
// Embed orderId in callback URL so FrontOffice can pass it back for verification
var separator = request.CallbackUrl.Contains('?') ? "&" : "?";
var callbackWithOrder = $"{request.CallbackUrl}{separator}orderId={purchase.Id}";
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
Amount = package.Price,
UserId = userId,
Mobile = user?.Mobile ?? string.Empty,
Description = $"خرید پکیج {package.Title}",
CallbackUrl = callbackWithOrder
}, context.CancellationToken);
if (!paymentResult.IsSuccess)
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerPurchasePackageResponse
{
Success = false,
Message = paymentResult.ErrorMessage ?? "خطا در ارتباط با درگاه پرداخت"
};
}
// Save RefId
transaction.RefId = paymentResult.RefId;
// ثبت PaymentTransaction
var paymentTx = new PaymentTransaction
{
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
Amount = package.Price,
CallbackUrl = request.CallbackUrl,
Description = $"خرید پکیج {package.Title}",
Mobile = user?.Mobile,
UserId = userId,
RequestStatusCode = 100,
RequestStatusMessage = "Success",
Authority = paymentResult.RefId,
PaymentStatus = false,
TransactionId = transaction.Id,
OrderId = purchase.Id.ToString()
};
_context.PaymentTransactions.Add(paymentTx);
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerPurchasePackageResponse
{
Success = true,
Message = "درخواست خرید پکیج با موفقیت ثبت شد",
OrderId = purchase.Id,
PaymentGatewayUrl = paymentResult.GatewayUrl ?? string.Empty,
Authority = paymentResult.RefId ?? string.Empty
};
}
public override async Task<CustomerVerifyPackagePurchaseResponse> CustomerVerifyPackagePurchase(CustomerVerifyPackagePurchaseRequest request, ServerCallContext context)
{
// Find purchase record
var purchase = await _context.UserPackagePurchases
.Include(p => p.Package)
.Where(p => p.Id == request.OrderId && !p.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (purchase == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
// Find the associated transaction
var transaction = purchase.TransactionId.HasValue
? await _context.Transactions
.Where(t => t.Id == purchase.TransactionId.Value)
.FirstOrDefaultAsync(context.CancellationToken)
: null;
// If status from gateway callback is not OK
if (request.Status != "OK")
{
if (transaction != null)
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
await _context.SaveChangesAsync(context.CancellationToken);
}
return new CustomerVerifyPackagePurchaseResponse
{
Success = false,
Message = "پرداخت توسط کاربر لغو شد"
};
}
// Verify with payment gateway
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority, request.Status, context.CancellationToken);
// آپدیت PaymentTransaction
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, context.CancellationToken);
if (paymentTx != null)
{
paymentTx.PaymentStatus = verifyResult.IsSuccess;
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
paymentTx.VerificationStatusMessage = verifyResult.Message;
paymentTx.CardPan = verifyResult.CardPan;
paymentTx.CardHash = verifyResult.CardHash;
paymentTx.RefId = verifyResult.TrackingCode;
}
if (verifyResult.IsSuccess && transaction != null)
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Success;
transaction.PaymentDate = DateTime.UtcNow;
transaction.RefId = verifyResult.RefId;
// شارژ کیف پول کاربر
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(w => w.UserId == purchase.UserId, context.CancellationToken);
if (wallet == null)
{
wallet = new CMSMicroservice.Domain.Entities.UserWallet
{
UserId = purchase.UserId,
Balance = 0,
DiscountBalance = 0,
NetworkBalance = 0
};
_context.UserWallets.Add(wallet);
await _context.SaveChangesAsync(context.CancellationToken);
}
var discountMultiplier = purchase.Package?.DiscountMultiplier
?? throw new RpcException(new Status(StatusCode.Internal, "پکیج سفارش یافت نشد"));
var discountAmount = (long)(purchase.Amount * (double)discountMultiplier);
wallet.Balance += purchase.Amount;
wallet.DiscountBalance += discountAmount;
// ثبت لاگ کیف پول
var walletLog = new CMSMicroservice.Domain.Entities.UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = purchase.Amount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = discountAmount,
IsIncrease = true,
RefrenceId = transaction.Id,
PackageId = purchase.PackageId
};
_context.UserWalletHistories.Add(walletLog);
// به‌روزرسانی کاربر
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Id == purchase.UserId, context.CancellationToken);
if (user != null)
user.PackagePurchaseMethod = Domain.Enums.PackagePurchaseMethod.DirectPurchase;
}
else if (transaction != null)
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
}
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerVerifyPackagePurchaseResponse
{
Success = verifyResult.IsSuccess,
Message = verifyResult.IsSuccess ? "خرید پکیج با موفقیت تایید شد" : (verifyResult.Message ?? "خرید پکیج ناموفق بود"),
TransactionId = transaction?.Id ?? 0,
ReferenceCode = verifyResult.RefId ?? string.Empty,
PurchaseInfo = verifyResult.IsSuccess ? new PackagePurchaseInfo
{
PackageId = purchase.PackageId,
PackageName = purchase.Package?.Title ?? string.Empty,
AmountPaid = purchase.Amount,
PurchaseDate = Timestamp.FromDateTime(DateTime.SpecifyKind(purchase.PurchasedAt, DateTimeKind.Utc))
} : null
};
}
private long GetCurrentUserId()
{
if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0)
return userId;
throw new RpcException(new Status(StatusCode.Unauthenticated, "لطفاً وارد حساب کاربری خود شوید"));
}
public override async Task<GetCustomerPurchaseHistoryResponse> GetCustomerPurchaseHistory(GetCustomerPurchaseHistoryRequest request, ServerCallContext context)
{
var query = new GetCustomerPurchaseHistoryQuery
{
UserId = request.UserId,
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
PackageTypeFilter = request.PackageTypeFilter != PackageTypeEnum.PackageTypeBasic
? (int?)request.PackageTypeFilter
: null,
FromDate = request.FromDate?.ToDateTime(),
ToDate = request.ToDate?.ToDateTime()
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerPurchaseHistoryResponse
{
MetaData = result.MetaData.Adapt<MetaData>()
};
foreach (var purchase in result.Purchases)
{
response.Purchases.Add(new PackagePurchaseHistory
{
Id = purchase.Id,
PackageId = purchase.PackageId,
PackageName = purchase.PackageName,
Amount = purchase.Amount,
PackageType = (PackageTypeEnum)purchase.PackageType,
PurchaseDate = Timestamp.FromDateTime(DateTime.SpecifyKind(purchase.PurchaseDate, DateTimeKind.Utc)),
ExpiryDate = purchase.ExpiryDate.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(purchase.ExpiryDate.Value, DateTimeKind.Utc))
: null,
Status = (PaymentStatusEnum)purchase.Status,
StatusMessage = purchase.StatusMessage,
ReferenceCode = purchase.ReferenceCode
});
}
return response;
}
}