Compare commits
10 Commits
59ed3857d4
...
39bbe59bbc
| Author | SHA1 | Date | |
|---|---|---|---|
| 39bbe59bbc | |||
| 5c852e499d | |||
| cbc1225562 | |||
| 6ad1341754 | |||
| e970e516ce | |||
| 9d17f174af | |||
| 29426022aa | |||
| 2b958bd214 | |||
| f31fb5ee39 | |||
| 0b7212ac1c |
@@ -1,4 +1,4 @@
|
|||||||
# CMS Microservice
|
# CMS Microservice
|
||||||
|
|
||||||
Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
|
Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
|
||||||
|
|
||||||
@@ -24,6 +24,24 @@ stringData:
|
|||||||
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
||||||
"UseSandbox": false
|
"UseSandbox": false
|
||||||
},
|
},
|
||||||
|
"TwentyCard": {
|
||||||
|
"BaseUrl": "https://gateway.20card.ir/ipg",
|
||||||
|
"TerminalId": "165674",
|
||||||
|
"TerminalUsername": "ea5y1biehi",
|
||||||
|
"TerminalPassword": "SaLamAT@1404!",
|
||||||
|
"CreditPortalUrl": "https://userpanel.20card.ir/",
|
||||||
|
"DefaultEmail": "info@kbs2.ir",
|
||||||
|
"Packages": {
|
||||||
|
"4": {
|
||||||
|
"GatewayAmountToman": 9700000,
|
||||||
|
"WalletCreditToman": 13000000
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"GatewayAmountToman": 97000000,
|
||||||
|
"WalletCreditToman": 130000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CmsBaseUrl": "https://cms.kbs2.ir",
|
"CmsBaseUrl": "https://cms.kbs2.ir",
|
||||||
"FrontOfficeBaseUrl": "https://kbs2.ir",
|
"FrontOfficeBaseUrl": "https://kbs2.ir",
|
||||||
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
||||||
|
|||||||
@@ -24,6 +24,25 @@ stringData:
|
|||||||
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
||||||
"UseSandbox": true
|
"UseSandbox": true
|
||||||
},
|
},
|
||||||
|
"TwentyCard": {
|
||||||
|
"BaseUrl": "https://gateway.20card.ir/ipg",
|
||||||
|
"TerminalId": "165674",
|
||||||
|
"TerminalUsername": "ea5y1biehi",
|
||||||
|
"TerminalPassword": "SaLamAT@1404!",
|
||||||
|
"CreditPortalUrl": "https://userpanel.20card.ir/",
|
||||||
|
"CallbackBaseUrl": "https://kbs2.ir",
|
||||||
|
"DefaultEmail": "info@kbs2.ir",
|
||||||
|
"Packages": {
|
||||||
|
"4": {
|
||||||
|
"GatewayAmountToman": 1000,
|
||||||
|
"WalletCreditToman": 13000000
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"GatewayAmountToman": 1000,
|
||||||
|
"WalletCreditToman": 130000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CmsBaseUrl": "https://cms.se.kbs1.ir",
|
"CmsBaseUrl": "https://cms.se.kbs1.ir",
|
||||||
"FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir",
|
"FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir",
|
||||||
"FMS": {
|
"FMS": {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.4.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.4.0" />
|
||||||
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.10" />
|
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.10" />
|
||||||
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public interface IApplicationDbContext
|
|||||||
DbSet<Product> Products { get; }
|
DbSet<Product> Products { get; }
|
||||||
DbSet<ProductImage> ProductImages { get; }
|
DbSet<ProductImage> ProductImages { get; }
|
||||||
DbSet<User> Users { get; }
|
DbSet<User> Users { get; }
|
||||||
|
DbSet<UserInvitation> UserInvitations { get; }
|
||||||
DbSet<OtpToken> OtpTokens { get; }
|
DbSet<OtpToken> OtpTokens { get; }
|
||||||
DbSet<Contract> Contracts { get; }
|
DbSet<Contract> Contracts { get; }
|
||||||
DbSet<UserContract> UserContracts { get; }
|
DbSet<UserContract> UserContracts { get; }
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolve payment gateway by provider key (zarinpal, 20card, …).
|
||||||
|
/// </summary>
|
||||||
|
public interface IPaymentGatewayResolver
|
||||||
|
{
|
||||||
|
IPaymentGatewayService Get(string? provider);
|
||||||
|
}
|
||||||
@@ -98,6 +98,26 @@ public class PaymentRequest
|
|||||||
/// URL بازگشت بعد از پرداخت
|
/// URL بازگشت بعد از پرداخت
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string CallbackUrl { get; set; } = string.Empty;
|
public string CallbackUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نام پرداختکننده (برای درگاههایی مثل ۲۰کارت)
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شماره فاکتور / سفارش
|
||||||
|
/// </summary>
|
||||||
|
public string Invoice { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ایمیل (اختیاری — برای ۲۰کارت)
|
||||||
|
/// </summary>
|
||||||
|
public string? Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// کد ملی (اختیاری — برای ۲۰کارت)
|
||||||
|
/// </summary>
|
||||||
|
public string? NationalId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+26
-8
@@ -8,6 +8,7 @@ using CMSMicroservice.Domain.Entities.Payment;
|
|||||||
using CMSMicroservice.Domain.Enums;
|
using CMSMicroservice.Domain.Enums;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
|
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
|
||||||
@@ -16,20 +17,23 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
|
|||||||
: IRequestHandler<VerifyUserPackagePurchasePaymentCommand, VerifyUserPackagePurchasePaymentResult>
|
: IRequestHandler<VerifyUserPackagePurchasePaymentCommand, VerifyUserPackagePurchasePaymentResult>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IPaymentGatewayService _paymentGateway;
|
private readonly IPaymentGatewayResolver _paymentGatewayResolver;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ISender _sender;
|
private readonly ISender _sender;
|
||||||
private readonly IUserPaymentLock _paymentLock;
|
private readonly IUserPaymentLock _paymentLock;
|
||||||
private readonly ILogger<VerifyUserPackagePurchasePaymentCommandHandler> _logger;
|
private readonly ILogger<VerifyUserPackagePurchasePaymentCommandHandler> _logger;
|
||||||
|
|
||||||
public VerifyUserPackagePurchasePaymentCommandHandler(
|
public VerifyUserPackagePurchasePaymentCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IPaymentGatewayService paymentGateway,
|
IPaymentGatewayResolver paymentGatewayResolver,
|
||||||
|
IConfiguration configuration,
|
||||||
ISender sender,
|
ISender sender,
|
||||||
IUserPaymentLock paymentLock,
|
IUserPaymentLock paymentLock,
|
||||||
ILogger<VerifyUserPackagePurchasePaymentCommandHandler> logger)
|
ILogger<VerifyUserPackagePurchasePaymentCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_paymentGateway = paymentGateway;
|
_paymentGatewayResolver = paymentGatewayResolver;
|
||||||
|
_configuration = configuration;
|
||||||
_sender = sender;
|
_sender = sender;
|
||||||
_paymentLock = paymentLock;
|
_paymentLock = paymentLock;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -105,7 +109,8 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
|
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
|
||||||
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
|
var gateway = _paymentGatewayResolver.Get(paymentTx?.GatewayProvider);
|
||||||
|
var verifyResult = await gateway.VerifyPaymentAsync(
|
||||||
request.Authority, request.Status, (decimal)amountInToman, ct);
|
request.Authority, request.Status, (decimal)amountInToman, ct);
|
||||||
|
|
||||||
if (!verifyResult.IsSuccess)
|
if (!verifyResult.IsSuccess)
|
||||||
@@ -193,17 +198,30 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
|
|||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
var discountMultiplier = purchase.Package?.DiscountMultiplier
|
var packagePrice = purchase.Package?.Price
|
||||||
?? throw new InvalidOperationException("پکیج سفارش یافت نشد");
|
?? throw new InvalidOperationException("پکیج سفارش یافت نشد");
|
||||||
var discountAmount = (long)(purchase.Amount * (double)discountMultiplier);
|
var discountMultiplier = purchase.Package.DiscountMultiplier;
|
||||||
wallet.Balance += purchase.Amount;
|
var discountAmount = (long)(packagePrice * (double)discountMultiplier);
|
||||||
|
|
||||||
|
var isTwentyCard = string.Equals(
|
||||||
|
paymentTx?.GatewayProvider, "20card", StringComparison.OrdinalIgnoreCase);
|
||||||
|
long balanceCredit = purchase.Amount;
|
||||||
|
if (isTwentyCard)
|
||||||
|
{
|
||||||
|
balanceCredit = _configuration.GetValue<long?>(
|
||||||
|
$"TwentyCard:Packages:{purchase.PackageId}:WalletCreditToman") ?? 0;
|
||||||
|
if (balanceCredit <= 0)
|
||||||
|
throw new InvalidOperationException("WalletCredit ۲۰کارت برای این پکیج پیکربندی نشده است");
|
||||||
|
}
|
||||||
|
|
||||||
|
wallet.Balance += balanceCredit;
|
||||||
wallet.DiscountBalance += discountAmount;
|
wallet.DiscountBalance += discountAmount;
|
||||||
|
|
||||||
_context.UserWalletHistories.Add(new UserWalletHistory
|
_context.UserWalletHistories.Add(new UserWalletHistory
|
||||||
{
|
{
|
||||||
WalletId = wallet.Id,
|
WalletId = wallet.Id,
|
||||||
CurrentBalance = wallet.Balance,
|
CurrentBalance = wallet.Balance,
|
||||||
ChangeValue = purchase.Amount,
|
ChangeValue = balanceCredit,
|
||||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||||
ChangeNerworkValue = 0,
|
ChangeNerworkValue = 0,
|
||||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||||
|
|||||||
+2
@@ -9,5 +9,7 @@ public record VerifyOtpTokenCommand : IRequest<VerifyOtpTokenResponseDto>
|
|||||||
public string Code { get; init; }
|
public string Code { get; init; }
|
||||||
//کد معرف والد
|
//کد معرف والد
|
||||||
public string? ParentReferralCode { get; init; }
|
public string? ParentReferralCode { get; init; }
|
||||||
|
//شماره موبایل دعوتکننده (اختیاری)
|
||||||
|
public string? InviterMobile { get; init; }
|
||||||
|
|
||||||
}
|
}
|
||||||
+37
@@ -118,6 +118,34 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
|
|||||||
else
|
else
|
||||||
return new VerifyOtpTokenResponseDto { Success = false, Message = "ظرفیت معرف تکمیل است!!" };
|
return new VerifyOtpTokenResponseDto { Success = false, Message = "ظرفیت معرف تکمیل است!!" };
|
||||||
|
|
||||||
|
// ── دعوتکننده (اختیاری؛ اعتبارسنجی سخت قبل از ایجاد کاربر) ──
|
||||||
|
User? inviter = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.InviterMobile))
|
||||||
|
{
|
||||||
|
var inviterMobile = request.InviterMobile.NormalizeIranMobile();
|
||||||
|
if (inviterMobile == mobile)
|
||||||
|
{
|
||||||
|
return new VerifyOtpTokenResponseDto
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "شماره موبایل دعوتکننده نمیتواند با شماره خودتان یکسان باشد."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inviter = await _context.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(u => u.Mobile == inviterMobile, cancellationToken);
|
||||||
|
|
||||||
|
if (inviter == null)
|
||||||
|
{
|
||||||
|
return new VerifyOtpTokenResponseDto
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "دعوتکننده با این شماره موبایل یافت نشد."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ایجاد کاربر جدید
|
// ایجاد کاربر جدید
|
||||||
user = new User
|
user = new User
|
||||||
{
|
{
|
||||||
@@ -134,6 +162,15 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
|
|||||||
user.AddDomainEvent(new CreateNewUserEvent(user));
|
user.AddDomainEvent(new CreateNewUserEvent(user));
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (inviter != null)
|
||||||
|
{
|
||||||
|
await _context.UserInvitations.AddAsync(new UserInvitation
|
||||||
|
{
|
||||||
|
InviterUserId = inviter.Id,
|
||||||
|
InviteeUserId = user.Id
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
// ایجاد نقش کاربری
|
// ایجاد نقش کاربری
|
||||||
var userRole = new UserRole { UserId = user.Id, RoleId = 1 };
|
var userRole = new UserRole { UserId = user.Id, RoleId = 1 };
|
||||||
await _context.UserRoles.AddAsync(userRole, cancellationToken);
|
await _context.UserRoles.AddAsync(userRole, cancellationToken);
|
||||||
|
|||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.UserCQ.Queries.GetInvitationsByFilter;
|
||||||
|
|
||||||
|
public class GetInvitationsByFilterQuery : IRequest<GetInvitationsByFilterResult>
|
||||||
|
{
|
||||||
|
public PaginationState? PaginationState { get; set; }
|
||||||
|
public string? SortBy { get; set; }
|
||||||
|
public GetInvitationsByFilterFilterDto? Filter { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetInvitationsByFilterFilterDto
|
||||||
|
{
|
||||||
|
public string? InviterMobile { get; set; }
|
||||||
|
public string? InviterFirstName { get; set; }
|
||||||
|
public string? InviterLastName { get; set; }
|
||||||
|
public string? InviteeMobile { get; set; }
|
||||||
|
public string? InviteeFirstName { get; set; }
|
||||||
|
public string? InviteeLastName { get; set; }
|
||||||
|
public bool? InviteeClubActive { get; set; }
|
||||||
|
public DateTime? CreatedFrom { get; set; }
|
||||||
|
public DateTime? CreatedTo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetInvitationsByFilterResult
|
||||||
|
{
|
||||||
|
public MetaData MetaData { get; set; } = new();
|
||||||
|
public List<InvitationReportDto> Models { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InvitationReportDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public long InviterUserId { get; set; }
|
||||||
|
public string? InviterFirstName { get; set; }
|
||||||
|
public string? InviterLastName { get; set; }
|
||||||
|
public string InviterMobile { get; set; } = string.Empty;
|
||||||
|
public long InviteeUserId { get; set; }
|
||||||
|
public string? InviteeFirstName { get; set; }
|
||||||
|
public string? InviteeLastName { get; set; }
|
||||||
|
public string InviteeMobile { get; set; } = string.Empty;
|
||||||
|
public DateTime InviteeRegisteredAt { get; set; }
|
||||||
|
public bool InviteeClubActive { get; set; }
|
||||||
|
public DateTime InvitedAt { get; set; }
|
||||||
|
}
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.UserCQ.Queries.GetInvitationsByFilter;
|
||||||
|
|
||||||
|
public class GetInvitationsByFilterQueryHandler
|
||||||
|
: IRequestHandler<GetInvitationsByFilterQuery, GetInvitationsByFilterResult>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetInvitationsByFilterQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetInvitationsByFilterResult> Handle(
|
||||||
|
GetInvitationsByFilterQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var filter = request.Filter ?? new GetInvitationsByFilterFilterDto();
|
||||||
|
|
||||||
|
var query = _context.UserInvitations
|
||||||
|
.AsNoTracking()
|
||||||
|
.Select(x => new InvitationReportDto
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
InviterUserId = x.InviterUserId,
|
||||||
|
InviterFirstName = x.Inviter.FirstName,
|
||||||
|
InviterLastName = x.Inviter.LastName,
|
||||||
|
InviterMobile = x.Inviter.Mobile,
|
||||||
|
InviteeUserId = x.InviteeUserId,
|
||||||
|
InviteeFirstName = x.Invitee.FirstName,
|
||||||
|
InviteeLastName = x.Invitee.LastName,
|
||||||
|
InviteeMobile = x.Invitee.Mobile,
|
||||||
|
InviteeRegisteredAt = x.Invitee.Created,
|
||||||
|
InviteeClubActive = x.Invitee.ClubMembership != null && x.Invitee.ClubMembership.IsActive,
|
||||||
|
InvitedAt = x.Created
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.InviterMobile))
|
||||||
|
query = query.Where(x => x.InviterMobile.Contains(filter.InviterMobile));
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.InviterFirstName))
|
||||||
|
query = query.Where(x => x.InviterFirstName != null && x.InviterFirstName.Contains(filter.InviterFirstName));
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.InviterLastName))
|
||||||
|
query = query.Where(x => x.InviterLastName != null && x.InviterLastName.Contains(filter.InviterLastName));
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.InviteeMobile))
|
||||||
|
query = query.Where(x => x.InviteeMobile.Contains(filter.InviteeMobile));
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.InviteeFirstName))
|
||||||
|
query = query.Where(x => x.InviteeFirstName != null && x.InviteeFirstName.Contains(filter.InviteeFirstName));
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.InviteeLastName))
|
||||||
|
query = query.Where(x => x.InviteeLastName != null && x.InviteeLastName.Contains(filter.InviteeLastName));
|
||||||
|
if (filter.InviteeClubActive.HasValue)
|
||||||
|
query = query.Where(x => x.InviteeClubActive == filter.InviteeClubActive.Value);
|
||||||
|
if (filter.CreatedFrom.HasValue)
|
||||||
|
query = query.Where(x => x.InvitedAt >= filter.CreatedFrom.Value);
|
||||||
|
if (filter.CreatedTo.HasValue)
|
||||||
|
query = query.Where(x => x.InvitedAt <= filter.CreatedTo.Value);
|
||||||
|
|
||||||
|
// پیشفرض: جدیدترین
|
||||||
|
query = (request.SortBy?.Trim().ToLowerInvariant()) switch
|
||||||
|
{
|
||||||
|
"oldest" or "created_asc" => query.OrderBy(x => x.InvitedAt),
|
||||||
|
_ => query.OrderByDescending(x => x.InvitedAt)
|
||||||
|
};
|
||||||
|
|
||||||
|
var paginationState = request.PaginationState ?? new PaginationState { PageNumber = 1, PageSize = 20 };
|
||||||
|
if (paginationState.PageNumber < 1) paginationState.PageNumber = 1;
|
||||||
|
if (paginationState.PageSize < 1) paginationState.PageSize = 20;
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
var models = await query
|
||||||
|
.Skip((paginationState.PageNumber - 1) * paginationState.PageSize)
|
||||||
|
.Take(paginationState.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var totalPage = totalCount == 0
|
||||||
|
? 0
|
||||||
|
: (int)Math.Ceiling((double)totalCount / paginationState.PageSize);
|
||||||
|
|
||||||
|
return new GetInvitationsByFilterResult
|
||||||
|
{
|
||||||
|
MetaData = new MetaData
|
||||||
|
{
|
||||||
|
TotalCount = totalCount,
|
||||||
|
CurrentPage = paginationState.PageNumber,
|
||||||
|
PageSize = paginationState.PageSize,
|
||||||
|
TotalPage = totalPage,
|
||||||
|
HasPrevious = paginationState.PageNumber > 1,
|
||||||
|
HasNext = paginationState.PageNumber < totalPage
|
||||||
|
},
|
||||||
|
Models = models
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.UserCQ.Queries.GetMyInvitees;
|
||||||
|
|
||||||
|
public class GetMyInviteesQuery : IRequest<GetMyInviteesResult>
|
||||||
|
{
|
||||||
|
public long UserId { get; set; }
|
||||||
|
public PaginationState? PaginationState { get; set; }
|
||||||
|
/// <summary>ALL | ACTIVE | INACTIVE — ClubMembership.IsActive</summary>
|
||||||
|
public string? ClubStatusFilter { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetMyInviteesResult
|
||||||
|
{
|
||||||
|
public MetaData MetaData { get; set; } = new();
|
||||||
|
public List<InviteeDto> Invitees { get; set; } = new();
|
||||||
|
public InviteeStatsDto Stats { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InviteeDto
|
||||||
|
{
|
||||||
|
public long UserId { get; set; }
|
||||||
|
public string? FirstName { get; set; }
|
||||||
|
public string? LastName { get; set; }
|
||||||
|
public string Mobile { get; set; } = string.Empty;
|
||||||
|
public DateTime RegisteredAt { get; set; }
|
||||||
|
public bool IsClubActive { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InviteeStatsDto
|
||||||
|
{
|
||||||
|
public int TotalInvitees { get; set; }
|
||||||
|
public int ClubActiveCount { get; set; }
|
||||||
|
public int ClubInactiveCount { get; set; }
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.UserCQ.Queries.GetMyInvitees;
|
||||||
|
|
||||||
|
public class GetMyInviteesQueryHandler : IRequestHandler<GetMyInviteesQuery, GetMyInviteesResult>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
|
public GetMyInviteesQueryHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetMyInviteesResult> Handle(GetMyInviteesQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userId = request.UserId == 0
|
||||||
|
? (long.TryParse(_currentUser.UserId, out var id) ? id : 0)
|
||||||
|
: request.UserId;
|
||||||
|
|
||||||
|
if (userId == 0)
|
||||||
|
throw new UnauthorizedAccessException("User ID not found in JWT token");
|
||||||
|
|
||||||
|
var baseQuery = _context.UserInvitations
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.InviterUserId == userId)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.InviteeUserId,
|
||||||
|
InviteeFirstName = x.Invitee.FirstName,
|
||||||
|
InviteeLastName = x.Invitee.LastName,
|
||||||
|
InviteeMobile = x.Invitee.Mobile,
|
||||||
|
RegisteredAt = x.Invitee.Created,
|
||||||
|
IsClubActive = x.Invitee.ClubMembership != null && x.Invitee.ClubMembership.IsActive,
|
||||||
|
InvitedAt = x.Created
|
||||||
|
});
|
||||||
|
|
||||||
|
var all = await baseQuery.ToListAsync(cancellationToken);
|
||||||
|
var total = all.Count;
|
||||||
|
var clubActive = all.Count(x => x.IsClubActive);
|
||||||
|
var clubInactive = total - clubActive;
|
||||||
|
|
||||||
|
var filtered = all.AsEnumerable();
|
||||||
|
var status = request.ClubStatusFilter?.Trim().ToUpperInvariant();
|
||||||
|
if (status == "ACTIVE")
|
||||||
|
filtered = filtered.Where(x => x.IsClubActive);
|
||||||
|
else if (status == "INACTIVE")
|
||||||
|
filtered = filtered.Where(x => !x.IsClubActive);
|
||||||
|
|
||||||
|
var ordered = filtered.OrderByDescending(x => x.InvitedAt).ToList();
|
||||||
|
|
||||||
|
var paginationState = request.PaginationState ?? new PaginationState { PageNumber = 1, PageSize = 10 };
|
||||||
|
if (paginationState.PageNumber < 1) paginationState.PageNumber = 1;
|
||||||
|
if (paginationState.PageSize < 1) paginationState.PageSize = 10;
|
||||||
|
|
||||||
|
var totalCount = ordered.Count;
|
||||||
|
var pageItems = ordered
|
||||||
|
.Skip((paginationState.PageNumber - 1) * paginationState.PageSize)
|
||||||
|
.Take(paginationState.PageSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var totalPage = totalCount == 0
|
||||||
|
? 0
|
||||||
|
: (int)Math.Ceiling((double)totalCount / paginationState.PageSize);
|
||||||
|
|
||||||
|
return new GetMyInviteesResult
|
||||||
|
{
|
||||||
|
MetaData = new MetaData
|
||||||
|
{
|
||||||
|
TotalCount = totalCount,
|
||||||
|
CurrentPage = paginationState.PageNumber,
|
||||||
|
PageSize = paginationState.PageSize,
|
||||||
|
TotalPage = totalPage,
|
||||||
|
HasPrevious = paginationState.PageNumber > 1,
|
||||||
|
HasNext = paginationState.PageNumber < totalPage
|
||||||
|
},
|
||||||
|
Stats = new InviteeStatsDto
|
||||||
|
{
|
||||||
|
TotalInvitees = total,
|
||||||
|
ClubActiveCount = clubActive,
|
||||||
|
ClubInactiveCount = clubInactive
|
||||||
|
},
|
||||||
|
Invitees = pageItems.Select(x => new InviteeDto
|
||||||
|
{
|
||||||
|
UserId = x.InviteeUserId,
|
||||||
|
FirstName = x.InviteeFirstName,
|
||||||
|
LastName = x.InviteeLastName,
|
||||||
|
Mobile = x.InviteeMobile,
|
||||||
|
RegisteredAt = x.RegisteredAt,
|
||||||
|
IsClubActive = x.IsClubActive
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace CMSMicroservice.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// رابطه دعوتکننده ↔ دعوتشونده (جدا از معرف شبکه / NetworkParent).
|
||||||
|
/// کد دعوت = شماره موبایل دعوتکننده.
|
||||||
|
/// </summary>
|
||||||
|
public class UserInvitation : BaseAuditableEntity
|
||||||
|
{
|
||||||
|
/// <summary>کاربری که دعوت کرده</summary>
|
||||||
|
public long InviterUserId { get; set; }
|
||||||
|
|
||||||
|
public virtual User Inviter { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>کاربری که دعوت شده (حداکثر یک دعوتکننده)</summary>
|
||||||
|
public long InviteeUserId { get; set; }
|
||||||
|
|
||||||
|
public virtual User Invitee { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -77,27 +77,33 @@ public static class ConfigureServices
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payment Gateway Service - Multi-Provider Architecture
|
// Payment Gateway — default provider + multi-gateway resolver (ZarinPal + TwentyCard coexist)
|
||||||
// پشتیبانی از درگاههای مختلف: ZarinPal, Daya, Mock
|
|
||||||
var paymentProvider = configuration.GetValue<string>("PaymentProvider", "Mock")?.ToLowerInvariant();
|
var paymentProvider = configuration.GetValue<string>("PaymentProvider", "Mock")?.ToLowerInvariant();
|
||||||
|
|
||||||
|
services.AddHttpClient<ZarinPalPaymentService>()
|
||||||
|
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||||
|
services.AddHttpClient<TwentyCardPaymentService>()
|
||||||
|
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||||
|
|
||||||
switch (paymentProvider)
|
switch (paymentProvider)
|
||||||
{
|
{
|
||||||
case "zarinpal":
|
case "zarinpal":
|
||||||
services.AddHttpClient<IPaymentGatewayService, ZarinPalPaymentService>()
|
services.AddScoped<IPaymentGatewayService>(sp => sp.GetRequiredService<ZarinPalPaymentService>());
|
||||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "daya":
|
case "daya":
|
||||||
services.AddHttpClient<IPaymentGatewayService, DayaPaymentService>()
|
services.AddHttpClient<DayaPaymentService>()
|
||||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||||
|
services.AddScoped<IPaymentGatewayService>(sp => sp.GetRequiredService<DayaPaymentService>());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "mock":
|
case "mock":
|
||||||
default:
|
default:
|
||||||
services.AddScoped<IPaymentGatewayService, MockPaymentGatewayService>();
|
services.AddScoped<IPaymentGatewayService, MockPaymentGatewayService>();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
services.AddScoped<IPaymentGatewayResolver, PaymentGatewayResolver>();
|
||||||
|
|
||||||
services.AddScoped<IApplicationDbContext>(p => p.GetRequiredService<ApplicationDbContext>());
|
services.AddScoped<IApplicationDbContext>(p => p.GetRequiredService<ApplicationDbContext>());
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
|
|||||||
public DbSet<Product> Products => Set<Product>();
|
public DbSet<Product> Products => Set<Product>();
|
||||||
public DbSet<ProductImage> ProductImages => Set<ProductImage>();
|
public DbSet<ProductImage> ProductImages => Set<ProductImage>();
|
||||||
public DbSet<User> Users => Set<User>();
|
public DbSet<User> Users => Set<User>();
|
||||||
|
public DbSet<UserInvitation> UserInvitations => Set<UserInvitation>();
|
||||||
public DbSet<OtpToken> OtpTokens => Set<OtpToken>();
|
public DbSet<OtpToken> OtpTokens => Set<OtpToken>();
|
||||||
public DbSet<Contract> Contracts => Set<Contract>();
|
public DbSet<Contract> Contracts => Set<Contract>();
|
||||||
public DbSet<UserContract> UserContracts => Set<UserContract>();
|
public DbSet<UserContract> UserContracts => Set<UserContract>();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
|
|||||||
builder.Property(entity => entity.Id).UseIdentityColumn();
|
builder.Property(entity => entity.Id).UseIdentityColumn();
|
||||||
builder.Property(entity => entity.FirstName).IsRequired(false);
|
builder.Property(entity => entity.FirstName).IsRequired(false);
|
||||||
builder.Property(entity => entity.LastName).IsRequired(false);
|
builder.Property(entity => entity.LastName).IsRequired(false);
|
||||||
builder.Property(entity => entity.Mobile).IsRequired(true);
|
builder.Property(entity => entity.Mobile).IsRequired(true).HasMaxLength(20);
|
||||||
builder.Property(entity => entity.NationalCode).IsRequired(false);
|
builder.Property(entity => entity.NationalCode).IsRequired(false);
|
||||||
builder.Property(entity => entity.AvatarPath).IsRequired(false);
|
builder.Property(entity => entity.AvatarPath).IsRequired(false);
|
||||||
builder.Property(entity => entity.ReferralCode).IsRequired(true);
|
builder.Property(entity => entity.ReferralCode).IsRequired(true);
|
||||||
@@ -48,5 +48,10 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
|
|||||||
builder.HasIndex(e => e.LegPosition)
|
builder.HasIndex(e => e.LegPosition)
|
||||||
.HasDatabaseName("IX_User_LegPosition");
|
.HasDatabaseName("IX_User_LegPosition");
|
||||||
|
|
||||||
|
// موبایل یکتا برای کاربران فعال (lookup دعوتکننده)
|
||||||
|
builder.HasIndex(e => e.Mobile)
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("[IsDeleted] = 0")
|
||||||
|
.HasDatabaseName("IX_User_Mobile_Active");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
|
||||||
|
|
||||||
|
public class UserInvitationConfiguration : IEntityTypeConfiguration<UserInvitation>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UserInvitation> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("UserInvitations");
|
||||||
|
builder.HasQueryFilter(p => !p.IsDeleted);
|
||||||
|
builder.Ignore(entity => entity.DomainEvents);
|
||||||
|
|
||||||
|
builder.HasKey(entity => entity.Id);
|
||||||
|
builder.Property(entity => entity.Id).UseIdentityColumn();
|
||||||
|
|
||||||
|
builder.Property(entity => entity.InviterUserId).IsRequired();
|
||||||
|
builder.Property(entity => entity.InviteeUserId).IsRequired();
|
||||||
|
|
||||||
|
builder.HasOne(entity => entity.Inviter)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(entity => entity.InviterUserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
builder.HasOne(entity => entity.Invitee)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(entity => entity.InviteeUserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
builder.HasIndex(e => e.InviteeUserId)
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_UserInvitations_InviteeUserId");
|
||||||
|
|
||||||
|
builder.HasIndex(e => new { e.InviterUserId, e.Created })
|
||||||
|
.HasDatabaseName("IX_UserInvitations_InviterUserId_Created");
|
||||||
|
}
|
||||||
|
}
|
||||||
+5269
File diff suppressed because it is too large
Load Diff
+103
@@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUserInvitations : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Mobile",
|
||||||
|
schema: "CMS",
|
||||||
|
table: "Users",
|
||||||
|
type: "nvarchar(20)",
|
||||||
|
maxLength: 20,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "nvarchar(max)");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserInvitations",
|
||||||
|
schema: "CMS",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
InviterUserId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
InviteeUserId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UserInvitations", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserInvitations_Users_InviteeUserId",
|
||||||
|
column: x => x.InviteeUserId,
|
||||||
|
principalSchema: "CMS",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserInvitations_Users_InviterUserId",
|
||||||
|
column: x => x.InviterUserId,
|
||||||
|
principalSchema: "CMS",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_User_Mobile_Active",
|
||||||
|
schema: "CMS",
|
||||||
|
table: "Users",
|
||||||
|
column: "Mobile",
|
||||||
|
unique: true,
|
||||||
|
filter: "[IsDeleted] = 0");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserInvitations_InviteeUserId",
|
||||||
|
schema: "CMS",
|
||||||
|
table: "UserInvitations",
|
||||||
|
column: "InviteeUserId",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserInvitations_InviterUserId_Created",
|
||||||
|
schema: "CMS",
|
||||||
|
table: "UserInvitations",
|
||||||
|
columns: new[] { "InviterUserId", "Created" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserInvitations",
|
||||||
|
schema: "CMS");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_User_Mobile_Active",
|
||||||
|
schema: "CMS",
|
||||||
|
table: "Users");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Mobile",
|
||||||
|
schema: "CMS",
|
||||||
|
table: "Users",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "nvarchar(20)",
|
||||||
|
oldMaxLength: 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+66
-1
@@ -3574,7 +3574,7 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
|||||||
|
|
||||||
b.Property<string>("Mobile")
|
b.Property<string>("Mobile")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
b.Property<DateTime?>("MobileVerifiedAt")
|
b.Property<DateTime?>("MobileVerifiedAt")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
@@ -3606,6 +3606,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
|||||||
b.HasIndex("LegPosition")
|
b.HasIndex("LegPosition")
|
||||||
.HasDatabaseName("IX_User_LegPosition");
|
.HasDatabaseName("IX_User_LegPosition");
|
||||||
|
|
||||||
|
b.HasIndex("Mobile")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_User_Mobile_Active")
|
||||||
|
.HasFilter("[IsDeleted] = 0");
|
||||||
|
|
||||||
b.HasIndex("NetworkParentId")
|
b.HasIndex("NetworkParentId")
|
||||||
.HasDatabaseName("IX_User_NetworkParentId");
|
.HasDatabaseName("IX_User_NetworkParentId");
|
||||||
|
|
||||||
@@ -3750,6 +3755,47 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
|||||||
b.ToTable("UserContracts", "CMS");
|
b.ToTable("UserContracts", "CMS");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserInvitation", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<long>("InviteeUserId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("InviterUserId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastModified")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("LastModifiedBy")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InviteeUserId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_UserInvitations_InviteeUserId");
|
||||||
|
|
||||||
|
b.HasIndex("InviterUserId", "Created")
|
||||||
|
.HasDatabaseName("IX_UserInvitations_InviterUserId_Created");
|
||||||
|
|
||||||
|
b.ToTable("UserInvitations", "CMS");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b =>
|
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -4871,6 +4917,25 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
|||||||
b.Navigation("User");
|
b.Navigation("User");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserInvitation", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CMSMicroservice.Domain.Entities.User", "Invitee")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("InviteeUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CMSMicroservice.Domain.Entities.User", "Inviter")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("InviterUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Invitee");
|
||||||
|
|
||||||
|
b.Navigation("Inviter");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b =>
|
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
|
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Infrastructure.Services.Payment;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Services.Payment;
|
||||||
|
|
||||||
|
public class PaymentGatewayResolver : IPaymentGatewayResolver
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public PaymentGatewayResolver(IServiceProvider serviceProvider, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPaymentGatewayService Get(string? provider)
|
||||||
|
{
|
||||||
|
var key = Normalize(provider)
|
||||||
|
?? Normalize(_configuration["PaymentProvider"])
|
||||||
|
?? "zarinpal";
|
||||||
|
|
||||||
|
return key switch
|
||||||
|
{
|
||||||
|
"20card" or "twentycard" => _serviceProvider.GetRequiredService<TwentyCardPaymentService>(),
|
||||||
|
"zarinpal" => _serviceProvider.GetRequiredService<ZarinPalPaymentService>(),
|
||||||
|
_ => _serviceProvider.GetRequiredService<IPaymentGatewayService>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Normalize(string? value)
|
||||||
|
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant();
|
||||||
|
}
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Services.Payment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// درگاه پرداخت بیستکارت (20Card IPG Merchant API).
|
||||||
|
/// مبلغ ورودی به تومان است. درخواستها بهصورت multipart/form-data ارسال میشوند.
|
||||||
|
/// </summary>
|
||||||
|
public class TwentyCardPaymentService : IPaymentGatewayService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<TwentyCardPaymentService> _logger;
|
||||||
|
private readonly string _terminalId;
|
||||||
|
private readonly string _terminalUsername;
|
||||||
|
private readonly string _terminalPassword;
|
||||||
|
private readonly string _defaultEmail;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
|
};
|
||||||
|
|
||||||
|
public TwentyCardPaymentService(
|
||||||
|
HttpClient httpClient,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<TwentyCardPaymentService> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
var baseUrl = configuration["TwentyCard:BaseUrl"]
|
||||||
|
?? throw new InvalidOperationException("TwentyCard:BaseUrl is not configured.");
|
||||||
|
_terminalId = configuration["TwentyCard:TerminalId"]
|
||||||
|
?? throw new InvalidOperationException("TwentyCard:TerminalId is not configured.");
|
||||||
|
_terminalUsername = configuration["TwentyCard:TerminalUsername"]
|
||||||
|
?? throw new InvalidOperationException("TwentyCard:TerminalUsername is not configured.");
|
||||||
|
_terminalPassword = configuration["TwentyCard:TerminalPassword"]
|
||||||
|
?? throw new InvalidOperationException("TwentyCard:TerminalPassword is not configured.");
|
||||||
|
_defaultEmail = configuration["TwentyCard:DefaultEmail"] ?? "info@kbs2.ir";
|
||||||
|
|
||||||
|
_httpClient.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
|
||||||
|
if (!_httpClient.DefaultRequestHeaders.Contains("Accept"))
|
||||||
|
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/json");
|
||||||
|
_logger.LogInformation("TwentyCard payment service initialized. BaseUrl={BaseUrl}", baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PaymentInitiateResult> InitiatePaymentAsync(
|
||||||
|
PaymentRequest request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var amount = ((long)request.Amount).ToString();
|
||||||
|
var payerName = string.IsNullOrWhiteSpace(request.Name)
|
||||||
|
? (string.IsNullOrWhiteSpace(request.Mobile) ? "کاربر" : request.Mobile)
|
||||||
|
: request.Name.Trim();
|
||||||
|
var invoice = string.IsNullOrWhiteSpace(request.Invoice)
|
||||||
|
? request.UserId.ToString()
|
||||||
|
: request.Invoice;
|
||||||
|
|
||||||
|
using var content = BuildForm(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["terminal_id"] = _terminalId,
|
||||||
|
["terminal_username"] = _terminalUsername,
|
||||||
|
["terminal_password"] = _terminalPassword,
|
||||||
|
["amount"] = amount,
|
||||||
|
["name"] = payerName,
|
||||||
|
["mobile"] = request.Mobile ?? string.Empty,
|
||||||
|
["invoice"] = invoice,
|
||||||
|
["callback"] = request.CallbackUrl,
|
||||||
|
["description"] = request.Description ?? string.Empty,
|
||||||
|
// بدون email درگاه ۲۰کارت بهجای validation، HTTP 500 برمیگرداند
|
||||||
|
["email"] = ResolveEmail(request.Email)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.NationalId))
|
||||||
|
AddFormField(content, "national_id", request.NationalId);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"TwentyCard payment request: Amount={Amount} Toman, User={UserId}, Invoice={Invoice}",
|
||||||
|
amount, request.UserId, invoice);
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("api/pgw/merchant/request", content, cancellationToken);
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"TwentyCard HTTP error on request: StatusCode={StatusCode}, Body={Body}",
|
||||||
|
(int)response.StatusCode, responseBody);
|
||||||
|
|
||||||
|
return new PaymentInitiateResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
ErrorMessage = $"خطای ارتباط با درگاه ۲۰کارت (HTTP {(int)response.StatusCode})"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<TwentyCardRequestResponse>(responseBody, JsonOptions);
|
||||||
|
var authority = result?.Data?.Authority;
|
||||||
|
var redirectUrl = result?.Data?.RedirectUrl;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(authority) || string.IsNullOrWhiteSpace(redirectUrl))
|
||||||
|
{
|
||||||
|
_logger.LogError("TwentyCard request failed. Body={Body}", responseBody);
|
||||||
|
return new PaymentInitiateResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
ErrorMessage = result?.Message
|
||||||
|
?? result?.Error?.Message
|
||||||
|
?? "ایجاد تراکنش ۲۰کارت ناموفق بود"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PaymentInitiateResult
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
RefId = authority,
|
||||||
|
GatewayUrl = redirectUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "TwentyCard InitiatePayment failed");
|
||||||
|
return new PaymentInitiateResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
ErrorMessage = "خطا در ارتباط با درگاه ۲۰کارت"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PaymentVerificationResult> VerifyPaymentAsync(
|
||||||
|
string refId,
|
||||||
|
string verificationToken,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
=> throw new NotImplementedException("TwentyCard requires VerifyPaymentAsync with amount.");
|
||||||
|
|
||||||
|
public async Task<PaymentVerificationResult> VerifyPaymentAsync(
|
||||||
|
string refId,
|
||||||
|
string verificationToken,
|
||||||
|
decimal amountInToman,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!string.Equals(verificationToken, "OK", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new PaymentVerificationResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
RefId = refId,
|
||||||
|
Message = "پرداخت توسط کاربر لغو شد یا ناموفق بود",
|
||||||
|
VerificationCode = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
using var content = BuildForm(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["terminal_id"] = _terminalId,
|
||||||
|
["terminal_username"] = _terminalUsername,
|
||||||
|
["terminal_password"] = _terminalPassword,
|
||||||
|
["authority"] = refId,
|
||||||
|
["amount"] = ((long)amountInToman).ToString()
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("api/pgw/merchant/verify", content, cancellationToken);
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"TwentyCard verify response: Authority={Authority}, StatusCode={StatusCode}, Body={Body}",
|
||||||
|
refId, (int)response.StatusCode, responseBody);
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<TwentyCardVerifyResponse>(responseBody, JsonOptions);
|
||||||
|
|
||||||
|
var paid = result?.Status?.Paid == true;
|
||||||
|
var verified = result?.Status?.Verified == true;
|
||||||
|
var noError = result?.Error != true;
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode && noError && paid && verified)
|
||||||
|
{
|
||||||
|
return new PaymentVerificationResult
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
RefId = refId,
|
||||||
|
TrackingCode = refId,
|
||||||
|
Message = result?.Message ?? "تراکنش با موفقیت تایید شد",
|
||||||
|
VerificationCode = result?.Code ?? 200
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code 103 = already verified — treat as success for idempotency
|
||||||
|
if (result?.Code == 103)
|
||||||
|
{
|
||||||
|
return new PaymentVerificationResult
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
RefId = refId,
|
||||||
|
TrackingCode = refId,
|
||||||
|
Message = result.Message ?? "تراکنش قبلاً تایید شده است",
|
||||||
|
VerificationCode = 103
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PaymentVerificationResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
RefId = refId,
|
||||||
|
Message = result?.Message ?? "تایید تراکنش ۲۰کارت ناموفق بود",
|
||||||
|
VerificationCode = result?.Code ?? (int)response.StatusCode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "TwentyCard VerifyPayment failed. Authority={Authority}", refId);
|
||||||
|
return new PaymentVerificationResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
RefId = refId,
|
||||||
|
Message = "خطا در تایید تراکنش ۲۰کارت"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PayoutResult> ProcessPayoutAsync(
|
||||||
|
PayoutRequest request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new PayoutResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
Message = "واریز از طریق درگاه ۲۰کارت پشتیبانی نمیشود"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MultipartFormDataContent BuildForm(Dictionary<string, string> fields)
|
||||||
|
{
|
||||||
|
var form = new MultipartFormDataContent();
|
||||||
|
foreach (var (key, value) in fields)
|
||||||
|
AddFormField(form, key, value ?? string.Empty);
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شبیه curl --form: بدون Content-Type روی هر part ( بعضی بکاندهای PHP با charset=utf-8 میشکنند).
|
||||||
|
/// </summary>
|
||||||
|
private static void AddFormField(MultipartFormDataContent form, string name, string value)
|
||||||
|
{
|
||||||
|
var part = new StringContent(value ?? string.Empty);
|
||||||
|
part.Headers.ContentType = null;
|
||||||
|
form.Add(part, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveEmail(string? email)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(email) && email.Contains('@', StringComparison.Ordinal))
|
||||||
|
return email.Trim();
|
||||||
|
return _defaultEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TwentyCardRequestResponse
|
||||||
|
{
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public TwentyCardRequestData? Data { get; set; }
|
||||||
|
public TwentyCardErrorBody? Error { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TwentyCardErrorBody
|
||||||
|
{
|
||||||
|
public string? Code { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TwentyCardRequestData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("authority")]
|
||||||
|
public string? Authority { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("redirectUrl")]
|
||||||
|
public string? RedirectUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TwentyCardVerifyResponse
|
||||||
|
{
|
||||||
|
public bool Error { get; set; }
|
||||||
|
public int Code { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public TwentyCardVerifyStatus? Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TwentyCardVerifyStatus
|
||||||
|
{
|
||||||
|
public bool Paid { get; set; }
|
||||||
|
public bool Verified { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>0.0.211</Version>
|
<Version>0.0.213</Version>
|
||||||
<DebugType>None</DebugType>
|
<DebugType>None</DebugType>
|
||||||
<DebugSymbols>False</DebugSymbols>
|
<DebugSymbols>False</DebugSymbols>
|
||||||
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||||
|
|||||||
@@ -249,6 +249,8 @@ message CustomerPurchasePackageRequest
|
|||||||
int64 package_id = 1;
|
int64 package_id = 1;
|
||||||
PurchaseMethodEnum purchase_method = 2;
|
PurchaseMethodEnum purchase_method = 2;
|
||||||
string callback_url = 3;
|
string callback_url = 3;
|
||||||
|
// "zarinpal" (default) | "20card"
|
||||||
|
string gateway_provider = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CustomerPurchasePackageResponse
|
message CustomerPurchasePackageResponse
|
||||||
|
|||||||
@@ -115,6 +115,16 @@ service UserContract
|
|||||||
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
rpc GetMyInvitees(GetMyInviteesRequest) returns (GetMyInviteesResponse){
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/Customer/GetMyInvitees"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
rpc GetInvitationsByFilter(GetInvitationsByFilterRequest) returns (GetInvitationsByFilterResponse){
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/GetInvitationsByFilter"
|
||||||
|
};
|
||||||
|
};
|
||||||
rpc UploadCustomerAvatar(UploadCustomerAvatarRequest) returns (UploadCustomerAvatarResponse){
|
rpc UploadCustomerAvatar(UploadCustomerAvatarRequest) returns (UploadCustomerAvatarResponse){
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
post: "/Customer/UploadAvatar"
|
post: "/Customer/UploadAvatar"
|
||||||
@@ -294,6 +304,8 @@ message VerifyOtpTokenRequest
|
|||||||
string purpose = 2;
|
string purpose = 2;
|
||||||
string code = 3;
|
string code = 3;
|
||||||
google.protobuf.StringValue parent_referral_code = 4;
|
google.protobuf.StringValue parent_referral_code = 4;
|
||||||
|
// شماره موبایل دعوتکننده (اختیاری؛ فقط برای ثبتنام جدید)
|
||||||
|
google.protobuf.StringValue inviter_mobile = 5;
|
||||||
}
|
}
|
||||||
message VerifyOtpTokenResponse
|
message VerifyOtpTokenResponse
|
||||||
{
|
{
|
||||||
@@ -466,3 +478,78 @@ message UpdateCustomerSettingsRequest
|
|||||||
google.protobuf.StringValue time_zone = 6;
|
google.protobuf.StringValue time_zone = 6;
|
||||||
google.protobuf.BoolValue two_factor_auth_enabled = 7;
|
google.protobuf.BoolValue two_factor_auth_enabled = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============= Inviter (دعوتکننده) — جدا از معرف شبکه =============
|
||||||
|
|
||||||
|
message GetMyInviteesRequest
|
||||||
|
{
|
||||||
|
city.PaginationState pagination_state = 1;
|
||||||
|
// ALL | ACTIVE | INACTIVE — بر اساس ClubMembership.IsActive
|
||||||
|
google.protobuf.StringValue club_status_filter = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetMyInviteesResponse
|
||||||
|
{
|
||||||
|
city.MetaData meta_data = 1;
|
||||||
|
repeated InviteeModel invitees = 2;
|
||||||
|
InviteeStats stats = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InviteeModel
|
||||||
|
{
|
||||||
|
int64 user_id = 1;
|
||||||
|
google.protobuf.StringValue first_name = 2;
|
||||||
|
google.protobuf.StringValue last_name = 3;
|
||||||
|
string mobile = 4;
|
||||||
|
google.protobuf.Timestamp registered_at = 5;
|
||||||
|
bool is_club_active = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InviteeStats
|
||||||
|
{
|
||||||
|
int32 total_invitees = 1;
|
||||||
|
int32 club_active_count = 2;
|
||||||
|
int32 club_inactive_count = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetInvitationsByFilterRequest
|
||||||
|
{
|
||||||
|
city.PaginationState pagination_state = 1;
|
||||||
|
google.protobuf.StringValue sort_by = 2;
|
||||||
|
GetInvitationsByFilterFilter filter = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetInvitationsByFilterFilter
|
||||||
|
{
|
||||||
|
google.protobuf.StringValue inviter_mobile = 1;
|
||||||
|
google.protobuf.StringValue inviter_first_name = 2;
|
||||||
|
google.protobuf.StringValue inviter_last_name = 3;
|
||||||
|
google.protobuf.StringValue invitee_mobile = 4;
|
||||||
|
google.protobuf.StringValue invitee_first_name = 5;
|
||||||
|
google.protobuf.StringValue invitee_last_name = 6;
|
||||||
|
google.protobuf.BoolValue invitee_club_active = 7;
|
||||||
|
google.protobuf.Timestamp created_from = 8;
|
||||||
|
google.protobuf.Timestamp created_to = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetInvitationsByFilterResponse
|
||||||
|
{
|
||||||
|
city.MetaData meta_data = 1;
|
||||||
|
repeated InvitationReportModel models = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InvitationReportModel
|
||||||
|
{
|
||||||
|
int64 id = 1;
|
||||||
|
int64 inviter_user_id = 2;
|
||||||
|
google.protobuf.StringValue inviter_first_name = 3;
|
||||||
|
google.protobuf.StringValue inviter_last_name = 4;
|
||||||
|
string inviter_mobile = 5;
|
||||||
|
int64 invitee_user_id = 6;
|
||||||
|
google.protobuf.StringValue invitee_first_name = 7;
|
||||||
|
google.protobuf.StringValue invitee_last_name = 8;
|
||||||
|
string invitee_mobile = 9;
|
||||||
|
google.protobuf.Timestamp invitee_registered_at = 10;
|
||||||
|
bool invitee_club_active = 11;
|
||||||
|
google.protobuf.Timestamp invited_at = 12;
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
private readonly ISender _sender;
|
private readonly ISender _sender;
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly ICurrentUserService _currentUserService;
|
private readonly ICurrentUserService _currentUserService;
|
||||||
private readonly IPaymentGatewayService _paymentGateway;
|
private readonly IPaymentGatewayResolver _paymentGatewayResolver;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly IUserPaymentLock _paymentLock;
|
private readonly IUserPaymentLock _paymentLock;
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
ISender sender,
|
ISender sender,
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
ICurrentUserService currentUserService,
|
ICurrentUserService currentUserService,
|
||||||
IPaymentGatewayService paymentGateway,
|
IPaymentGatewayResolver paymentGatewayResolver,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IUserPaymentLock paymentLock)
|
IUserPaymentLock paymentLock)
|
||||||
{
|
{
|
||||||
@@ -49,7 +49,7 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
_sender = sender;
|
_sender = sender;
|
||||||
_context = context;
|
_context = context;
|
||||||
_currentUserService = currentUserService;
|
_currentUserService = currentUserService;
|
||||||
_paymentGateway = paymentGateway;
|
_paymentGatewayResolver = paymentGatewayResolver;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_paymentLock = paymentLock;
|
_paymentLock = paymentLock;
|
||||||
}
|
}
|
||||||
@@ -171,19 +171,38 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
|
|
||||||
if (package == null)
|
if (package == null)
|
||||||
throw new RpcException(new Status(StatusCode.NotFound, "پکیج مورد نظر یافت نشد"));
|
throw new RpcException(new Status(StatusCode.NotFound, "پکیج مورد نظر یافت نشد"));
|
||||||
|
|
||||||
// Create transaction
|
var gatewayProvider = NormalizeGatewayProvider(request.GatewayProvider);
|
||||||
|
var isTwentyCard = gatewayProvider == "20card";
|
||||||
|
|
||||||
|
long gatewayAmount = package.Price;
|
||||||
|
if (isTwentyCard)
|
||||||
|
{
|
||||||
|
var packageKey = $"TwentyCard:Packages:{package.Id}";
|
||||||
|
gatewayAmount = _configuration.GetValue<long?>($"{packageKey}:GatewayAmountToman") ?? 0;
|
||||||
|
var walletCredit = _configuration.GetValue<long?>($"{packageKey}:WalletCreditToman") ?? 0;
|
||||||
|
if (gatewayAmount <= 0 || walletCredit <= 0)
|
||||||
|
{
|
||||||
|
throw new RpcException(new Status(
|
||||||
|
StatusCode.FailedPrecondition,
|
||||||
|
"پرداخت ۲۰کارت برای این پکیج پیکربندی نشده است"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create transaction — 20card: Amount = gateway charge; zarinpal: Package.Price
|
||||||
var transaction = new CMSMicroservice.Domain.Entities.Transaction
|
var transaction = new CMSMicroservice.Domain.Entities.Transaction
|
||||||
{
|
{
|
||||||
Amount = package.Price,
|
Amount = gatewayAmount,
|
||||||
Description = $"خرید پکیج {package.Title}",
|
Description = isTwentyCard
|
||||||
|
? $"خرید پکیج {package.Title} (۲۰کارت)"
|
||||||
|
: $"خرید پکیج {package.Title}",
|
||||||
PaymentStatus = Domain.Enums.PaymentStatus.Pending,
|
PaymentStatus = Domain.Enums.PaymentStatus.Pending,
|
||||||
Type = Domain.Enums.TransactionType.Buy
|
Type = Domain.Enums.TransactionType.Buy
|
||||||
};
|
};
|
||||||
_context.Transactions.Add(transaction);
|
_context.Transactions.Add(transaction);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// Create purchase record
|
// Create purchase record — UPP.Amount always Package.Price (catalog)
|
||||||
var purchaseMethod = request.PurchaseMethod == PurchaseMethodEnum.PurchaseMethodGateway
|
var purchaseMethod = request.PurchaseMethod == PurchaseMethodEnum.PurchaseMethodGateway
|
||||||
? Domain.Enums.PackagePurchaseMethod.DirectPurchase
|
? Domain.Enums.PackagePurchaseMethod.DirectPurchase
|
||||||
: Domain.Enums.PackagePurchaseMethod.DayaLoan;
|
: Domain.Enums.PackagePurchaseMethod.DayaLoan;
|
||||||
@@ -204,20 +223,32 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
var user = await _context.Users
|
var user = await _context.Users
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(u => u.Id == userId)
|
.Where(u => u.Id == userId)
|
||||||
.Select(u => new { u.Mobile })
|
.Select(u => new { u.Mobile, u.FirstName, u.LastName, u.Email, u.NationalCode })
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
// Callback URL از config — نه از ورودی کاربر (امنیت)
|
// Callback URL از config — نه از ورودی کاربر (امنیت)
|
||||||
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
|
// ۲۰کارت: CallbackBaseUrl اختیاری (مثلاً استیج → kbs2.ir چون دامنه استیج در ترمینال ثبت نیست)
|
||||||
var callbackUrl = $"{frontOfficeBaseUrl}/profile/payment-callback?orderId={purchase.Id}";
|
var frontOfficeBaseUrl = isTwentyCard
|
||||||
|
? (_configuration["TwentyCard:CallbackBaseUrl"]
|
||||||
|
?? _configuration["FrontOfficeBaseUrl"]
|
||||||
|
?? "https://localhost:5268")
|
||||||
|
: (_configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268");
|
||||||
|
var callbackUrl = $"{frontOfficeBaseUrl.TrimEnd('/')}/profile/payment-callback?orderId={purchase.Id}";
|
||||||
|
|
||||||
|
var paymentGateway = _paymentGatewayResolver.Get(gatewayProvider);
|
||||||
|
var payerName = $"{user?.FirstName} {user?.LastName}".Trim();
|
||||||
|
|
||||||
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
|
var paymentResult = await paymentGateway.InitiatePaymentAsync(new PaymentRequest
|
||||||
{
|
{
|
||||||
Amount = package.Price,
|
Amount = gatewayAmount,
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
Mobile = user?.Mobile ?? string.Empty,
|
Mobile = user?.Mobile ?? string.Empty,
|
||||||
|
Name = payerName,
|
||||||
|
Invoice = purchase.Id.ToString(),
|
||||||
Description = $"خرید پکیج {package.Title}",
|
Description = $"خرید پکیج {package.Title}",
|
||||||
CallbackUrl = callbackUrl
|
CallbackUrl = callbackUrl,
|
||||||
|
Email = user?.Email,
|
||||||
|
NationalId = user?.NationalCode
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
if (!paymentResult.IsSuccess)
|
if (!paymentResult.IsSuccess)
|
||||||
@@ -235,12 +266,14 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
// Save RefId
|
// Save RefId
|
||||||
transaction.RefId = paymentResult.RefId;
|
transaction.RefId = paymentResult.RefId;
|
||||||
|
|
||||||
// ثبت PaymentTransaction
|
// ثبت PaymentTransaction — Amount = gateway amount
|
||||||
var paymentTx = new PaymentTransaction
|
var paymentTx = new PaymentTransaction
|
||||||
{
|
{
|
||||||
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
|
GatewayProvider = gatewayProvider,
|
||||||
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
|
MerchantId = isTwentyCard
|
||||||
Amount = package.Price,
|
? (_configuration["TwentyCard:TerminalId"] ?? "")
|
||||||
|
: (_configuration["ZarinPal:MerchantId"] ?? ""),
|
||||||
|
Amount = gatewayAmount,
|
||||||
CallbackUrl = callbackUrl,
|
CallbackUrl = callbackUrl,
|
||||||
Description = $"خرید پکیج {package.Title}",
|
Description = $"خرید پکیج {package.Title}",
|
||||||
Mobile = user?.Mobile,
|
Mobile = user?.Mobile,
|
||||||
@@ -264,6 +297,14 @@ public class PackageService : PackageContract.PackageContractBase
|
|||||||
Authority = paymentResult.RefId ?? string.Empty
|
Authority = paymentResult.RefId ?? string.Empty
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizeGatewayProvider(string? provider)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(provider))
|
||||||
|
return "zarinpal";
|
||||||
|
var key = provider.Trim().ToLowerInvariant();
|
||||||
|
return key is "20card" or "twentycard" ? "20card" : "zarinpal";
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task<CustomerVerifyPackagePurchaseResponse> CustomerVerifyPackagePurchase(
|
public override async Task<CustomerVerifyPackagePurchaseResponse> CustomerVerifyPackagePurchase(
|
||||||
CustomerVerifyPackagePurchaseRequest request,
|
CustomerVerifyPackagePurchaseRequest request,
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ using CMSMicroservice.Application.UserCQ.Commands.VerifyOtpToken;
|
|||||||
using CMSMicroservice.Application.UserCQ.Commands.AcceptContract;
|
using CMSMicroservice.Application.UserCQ.Commands.AcceptContract;
|
||||||
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerProfile;
|
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerProfile;
|
||||||
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerReferrals;
|
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerReferrals;
|
||||||
|
using CMSMicroservice.Application.UserCQ.Queries.GetMyInvitees;
|
||||||
|
using CMSMicroservice.Application.UserCQ.Queries.GetInvitationsByFilter;
|
||||||
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerSettings;
|
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerSettings;
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common.Authorization;
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
@@ -312,6 +315,117 @@ public class UserService : UserContract.UserContractBase
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task<GetMyInviteesResponse> GetMyInvitees(
|
||||||
|
GetMyInviteesRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
var result = await _sender.Send(new GetMyInviteesQuery
|
||||||
|
{
|
||||||
|
UserId = 0,
|
||||||
|
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
|
||||||
|
ClubStatusFilter = request.ClubStatusFilter
|
||||||
|
}, context.CancellationToken);
|
||||||
|
|
||||||
|
var response = new GetMyInviteesResponse
|
||||||
|
{
|
||||||
|
MetaData = new MetaData
|
||||||
|
{
|
||||||
|
CurrentPage = result.MetaData.CurrentPage,
|
||||||
|
TotalPage = result.MetaData.TotalPage,
|
||||||
|
PageSize = result.MetaData.PageSize,
|
||||||
|
TotalCount = result.MetaData.TotalCount,
|
||||||
|
HasPrevious = result.MetaData.HasPrevious,
|
||||||
|
HasNext = result.MetaData.HasNext
|
||||||
|
},
|
||||||
|
Stats = new InviteeStats
|
||||||
|
{
|
||||||
|
TotalInvitees = result.Stats.TotalInvitees,
|
||||||
|
ClubActiveCount = result.Stats.ClubActiveCount,
|
||||||
|
ClubInactiveCount = result.Stats.ClubInactiveCount
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var item in result.Invitees)
|
||||||
|
{
|
||||||
|
response.Invitees.Add(new InviteeModel
|
||||||
|
{
|
||||||
|
UserId = item.UserId,
|
||||||
|
FirstName = item.FirstName,
|
||||||
|
LastName = item.LastName,
|
||||||
|
Mobile = item.Mobile,
|
||||||
|
RegisteredAt = Timestamp.FromDateTime(DateTime.SpecifyKind(item.RegisteredAt, DateTimeKind.Utc)),
|
||||||
|
IsClubActive = item.IsClubActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RequiresPermission(PermissionNames.UsersView)]
|
||||||
|
public override async Task<GetInvitationsByFilterResponse> GetInvitationsByFilter(
|
||||||
|
GetInvitationsByFilterRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
DateTime? createdFrom = null;
|
||||||
|
DateTime? createdTo = null;
|
||||||
|
if (request.Filter?.CreatedFrom != null)
|
||||||
|
createdFrom = request.Filter.CreatedFrom.ToDateTime();
|
||||||
|
if (request.Filter?.CreatedTo != null)
|
||||||
|
createdTo = request.Filter.CreatedTo.ToDateTime();
|
||||||
|
|
||||||
|
var result = await _sender.Send(new GetInvitationsByFilterQuery
|
||||||
|
{
|
||||||
|
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
|
||||||
|
SortBy = request.SortBy,
|
||||||
|
Filter = request.Filter == null ? null : new GetInvitationsByFilterFilterDto
|
||||||
|
{
|
||||||
|
InviterMobile = request.Filter.InviterMobile,
|
||||||
|
InviterFirstName = request.Filter.InviterFirstName,
|
||||||
|
InviterLastName = request.Filter.InviterLastName,
|
||||||
|
InviteeMobile = request.Filter.InviteeMobile,
|
||||||
|
InviteeFirstName = request.Filter.InviteeFirstName,
|
||||||
|
InviteeLastName = request.Filter.InviteeLastName,
|
||||||
|
InviteeClubActive = request.Filter.InviteeClubActive,
|
||||||
|
CreatedFrom = createdFrom,
|
||||||
|
CreatedTo = createdTo
|
||||||
|
}
|
||||||
|
}, context.CancellationToken);
|
||||||
|
|
||||||
|
var response = new GetInvitationsByFilterResponse
|
||||||
|
{
|
||||||
|
MetaData = new MetaData
|
||||||
|
{
|
||||||
|
CurrentPage = result.MetaData.CurrentPage,
|
||||||
|
TotalPage = result.MetaData.TotalPage,
|
||||||
|
PageSize = result.MetaData.PageSize,
|
||||||
|
TotalCount = result.MetaData.TotalCount,
|
||||||
|
HasPrevious = result.MetaData.HasPrevious,
|
||||||
|
HasNext = result.MetaData.HasNext
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var m in result.Models)
|
||||||
|
{
|
||||||
|
response.Models.Add(new InvitationReportModel
|
||||||
|
{
|
||||||
|
Id = m.Id,
|
||||||
|
InviterUserId = m.InviterUserId,
|
||||||
|
InviterFirstName = m.InviterFirstName,
|
||||||
|
InviterLastName = m.InviterLastName,
|
||||||
|
InviterMobile = m.InviterMobile,
|
||||||
|
InviteeUserId = m.InviteeUserId,
|
||||||
|
InviteeFirstName = m.InviteeFirstName,
|
||||||
|
InviteeLastName = m.InviteeLastName,
|
||||||
|
InviteeMobile = m.InviteeMobile,
|
||||||
|
InviteeRegisteredAt = Timestamp.FromDateTime(DateTime.SpecifyKind(m.InviteeRegisteredAt, DateTimeKind.Utc)),
|
||||||
|
InviteeClubActive = m.InviteeClubActive,
|
||||||
|
InvitedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(m.InvitedAt, DateTimeKind.Utc))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task<UploadCustomerAvatarResponse> UploadCustomerAvatar(UploadCustomerAvatarRequest request, ServerCallContext context)
|
public override async Task<UploadCustomerAvatarResponse> UploadCustomerAvatar(UploadCustomerAvatarRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
if (request.FileData == null || request.FileData.Length == 0)
|
if (request.FileData == null || request.FileData.Length == 0)
|
||||||
|
|||||||
@@ -4,6 +4,24 @@
|
|||||||
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
||||||
"UseSandbox": false
|
"UseSandbox": false
|
||||||
},
|
},
|
||||||
|
"TwentyCard": {
|
||||||
|
"BaseUrl": "https://gateway.20card.ir/ipg",
|
||||||
|
"TerminalId": "165674",
|
||||||
|
"TerminalUsername": "ea5y1biehi",
|
||||||
|
"TerminalPassword": "SaLamAT@1404!",
|
||||||
|
"CreditPortalUrl": "https://userpanel.20card.ir/",
|
||||||
|
"DefaultEmail": "info@kbs2.ir",
|
||||||
|
"Packages": {
|
||||||
|
"4": {
|
||||||
|
"GatewayAmountToman": 9700000,
|
||||||
|
"WalletCreditToman": 13000000
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"GatewayAmountToman": 97000000,
|
||||||
|
"WalletCreditToman": 130000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CmsBaseUrl": "https://cms.kbs2.ir",
|
"CmsBaseUrl": "https://cms.kbs2.ir",
|
||||||
"FrontOfficeBaseUrl": "https://kbs2.ir",
|
"FrontOfficeBaseUrl": "https://kbs2.ir",
|
||||||
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"PaymentProvider": "zarinpal",
|
||||||
|
"ZarinPal": {
|
||||||
|
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
||||||
|
"UseSandbox": true
|
||||||
|
},
|
||||||
|
"TwentyCard": {
|
||||||
|
"BaseUrl": "https://gateway.20card.ir/ipg",
|
||||||
|
"TerminalId": "165674",
|
||||||
|
"TerminalUsername": "ea5y1biehi",
|
||||||
|
"TerminalPassword": "SaLamAT@1404!",
|
||||||
|
"CreditPortalUrl": "https://userpanel.20card.ir/",
|
||||||
|
"CallbackBaseUrl": "https://kbs2.ir",
|
||||||
|
"DefaultEmail": "info@kbs2.ir",
|
||||||
|
"Packages": {
|
||||||
|
"4": {
|
||||||
|
"GatewayAmountToman": 1000,
|
||||||
|
"WalletCreditToman": 13000000
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"GatewayAmountToman": 1000,
|
||||||
|
"WalletCreditToman": 130000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CmsBaseUrl": "https://cms.se.kbs1.ir",
|
||||||
|
"FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir",
|
||||||
|
"FMS": {
|
||||||
|
"Address": "https://dl.afrino.co"
|
||||||
|
},
|
||||||
|
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
||||||
|
"JwtIssuer": "https://localhost",
|
||||||
|
"JwtAudience": "https://localhost",
|
||||||
|
"JwtExpiryInDays": 5,
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Data Source=194.5.195.53,31433; Initial Catalog=Foursat;User ID=sa;Password=87zH26nbqT;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
|
||||||
|
"providerName": "System.Data.SqlClient"
|
||||||
|
},
|
||||||
|
"Otp": {
|
||||||
|
"Secret": "K2w8k1h1mH2Qz1kqWk0c8kQ2Pq8q9H1eE2nqN1qQ8x7M=",
|
||||||
|
"MasterCode": "1122334455"
|
||||||
|
},
|
||||||
|
"Monitoring": {
|
||||||
|
"SentryEnabled": false,
|
||||||
|
"SentryDsn": "",
|
||||||
|
"SlackEnabled": false,
|
||||||
|
"SlackWebhookUrl": "",
|
||||||
|
"EmailAlertsEnabled": false,
|
||||||
|
"AdminEmails": [
|
||||||
|
"admin@example.com"
|
||||||
|
],
|
||||||
|
"SmsNotificationsEnabled": false,
|
||||||
|
"SmsApiKey": "",
|
||||||
|
"SmsGatewayUrl": ""
|
||||||
|
},
|
||||||
|
"Email": {
|
||||||
|
"Enabled": true,
|
||||||
|
"SmtpHost": "smtp.gmail.com",
|
||||||
|
"SmtpPort": 587,
|
||||||
|
"SmtpUsername": "your-email@gmail.com",
|
||||||
|
"SmtpPassword": "your-app-password",
|
||||||
|
"FromEmail": "noreply@foursat.com",
|
||||||
|
"FromName": "FourSat CMS",
|
||||||
|
"EnableSsl": true
|
||||||
|
},
|
||||||
|
"Sms": {
|
||||||
|
"Enabled": true,
|
||||||
|
"Provider": "Kavenegar",
|
||||||
|
"KavenegarApiKey": "43676A4E786A6C50452F2F6252507939346A5145764B33566B456454374E657A614468706658656D6534593D",
|
||||||
|
"Sender": "1000001110100"
|
||||||
|
},
|
||||||
|
"DayaPayment": {
|
||||||
|
"BaseUrl": "https://api.daya.ir",
|
||||||
|
"ApiKey": "YOUR_DAYA_API_KEY"
|
||||||
|
},
|
||||||
|
"DayaApi": {
|
||||||
|
"UseMock": false,
|
||||||
|
"BaseAddress": "https://Dayadiamond.ir",
|
||||||
|
"MerchantPermissionKey": "56146364$04sXjethI5WxhItR1Q9xnmFdJzl2BB8Bclsq8dAy7YVSZp3vtt-wP7ivrcCvmKLq",
|
||||||
|
"CacheDurationMinutes": 20
|
||||||
|
},
|
||||||
|
"Chatika": {
|
||||||
|
"Enabled": true,
|
||||||
|
"BaseUrl": "https://api.chatika.ir",
|
||||||
|
"ApiKey": "tIukvL8dnV4cB3yVWcCD9Xyfbj8rBxm5wPt2mLyJCgTsBBoMTWjt6mFEqQwpw-er"
|
||||||
|
},
|
||||||
|
"BackgroundJobs": {
|
||||||
|
"WeeklyCommissionCalculation": {
|
||||||
|
"Enabled": true,
|
||||||
|
"CronExpression": "5 0 * * 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SeedWorkers": {
|
||||||
|
"MagicWalletCycleSeed": {
|
||||||
|
"Enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Kestrel": {
|
||||||
|
"EndpointDefaults": {
|
||||||
|
"Protocols": "Http2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Authentication": {
|
||||||
|
"Authority": "https://ids.domain.com/",
|
||||||
|
"Audience": "domain_api"
|
||||||
|
},
|
||||||
|
"Seq": {
|
||||||
|
"ServerUrl": "https://seq.afrino.co",
|
||||||
|
"ApiKey": "oxpvpUzU1pZxMS4s3Fqq"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,24 @@
|
|||||||
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
|
||||||
"UseSandbox": false
|
"UseSandbox": false
|
||||||
},
|
},
|
||||||
|
"TwentyCard": {
|
||||||
|
"BaseUrl": "https://gateway.20card.ir/ipg",
|
||||||
|
"TerminalId": "165674",
|
||||||
|
"TerminalUsername": "ea5y1biehi",
|
||||||
|
"TerminalPassword": "SaLamAT@1404!",
|
||||||
|
"CreditPortalUrl": "https://userpanel.20card.ir/",
|
||||||
|
"DefaultEmail": "info@kbs2.ir",
|
||||||
|
"Packages": {
|
||||||
|
"4": {
|
||||||
|
"GatewayAmountToman": 9700000,
|
||||||
|
"WalletCreditToman": 13000000
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"GatewayAmountToman": 97000000,
|
||||||
|
"WalletCreditToman": 130000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CmsBaseUrl": "https://cms.kbs2.ir",
|
"CmsBaseUrl": "https://cms.kbs2.ir",
|
||||||
"FrontOfficeBaseUrl": "https://kbs2.ir",
|
"FrontOfficeBaseUrl": "https://kbs2.ir",
|
||||||
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
||||||
|
|||||||
Reference in New Issue
Block a user