diff --git a/README.md b/README.md
index b4153ef..1a3fdd9 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
# CMS Microservice
-Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
-
\ No newline at end of file
+Documentation moved to [totalDoc/INDEX.md](../totalDoc/INDEX.md).
+
\ No newline at end of file
diff --git a/k8s/production/cms-config.yaml b/k8s/production/cms-config.yaml
index 6ce28ee..a1a0674 100644
--- a/k8s/production/cms-config.yaml
+++ b/k8s/production/cms-config.yaml
@@ -24,6 +24,24 @@ stringData:
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
"UseSandbox": false
},
+ "TwentyCard": {
+ "BaseUrl": "https://gateway.20card.ir/ipg",
+ "TerminalId": "165674",
+ "TerminalUsername": "ea5y1biehi",
+ "TerminalPassword": "SaLamAT@1404!",
+ "CreditPortalUrl": "https://userpanel.20card.ir/",
+ "DefaultEmail": "info@kbs2.ir",
+ "Packages": {
+ "4": {
+ "GatewayAmountToman": 9700000,
+ "WalletCreditToman": 13000000
+ },
+ "1": {
+ "GatewayAmountToman": 97000000,
+ "WalletCreditToman": 130000000
+ }
+ }
+ },
"CmsBaseUrl": "https://cms.kbs2.ir",
"FrontOfficeBaseUrl": "https://kbs2.ir",
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
diff --git a/k8s/staging/cms-config.yaml b/k8s/staging/cms-config.yaml
index c9c5562..fa9b9e3 100644
--- a/k8s/staging/cms-config.yaml
+++ b/k8s/staging/cms-config.yaml
@@ -24,6 +24,25 @@ stringData:
"MerchantId": "4225d555-5fa9-4df0-9b61-1ce152cbbba8",
"UseSandbox": true
},
+ "TwentyCard": {
+ "BaseUrl": "https://gateway.20card.ir/ipg",
+ "TerminalId": "165674",
+ "TerminalUsername": "ea5y1biehi",
+ "TerminalPassword": "SaLamAT@1404!",
+ "CreditPortalUrl": "https://userpanel.20card.ir/",
+ "CallbackBaseUrl": "https://kbs2.ir",
+ "DefaultEmail": "info@kbs2.ir",
+ "Packages": {
+ "4": {
+ "GatewayAmountToman": 1000,
+ "WalletCreditToman": 13000000
+ },
+ "1": {
+ "GatewayAmountToman": 1000,
+ "WalletCreditToman": 130000000
+ }
+ }
+ },
"CmsBaseUrl": "https://cms.se.kbs1.ir",
"FrontOfficeBaseUrl": "https://frontoffice.se.kbs1.ir",
"FMS": {
diff --git a/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj b/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj
index b364983..5b33f39 100644
--- a/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj
+++ b/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj
@@ -15,6 +15,7 @@
+
diff --git a/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs b/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs
index 3c6ca1b..6184676 100644
--- a/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs
+++ b/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs
@@ -21,6 +21,7 @@ public interface IApplicationDbContext
DbSet Products { get; }
DbSet ProductImages { get; }
DbSet Users { get; }
+ DbSet UserInvitations { get; }
DbSet OtpTokens { get; }
DbSet Contracts { get; }
DbSet UserContracts { get; }
diff --git a/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayResolver.cs b/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayResolver.cs
new file mode 100644
index 0000000..2c16d14
--- /dev/null
+++ b/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayResolver.cs
@@ -0,0 +1,9 @@
+namespace CMSMicroservice.Application.Common.Interfaces;
+
+///
+/// Resolve payment gateway by provider key (zarinpal, 20card, …).
+///
+public interface IPaymentGatewayResolver
+{
+ IPaymentGatewayService Get(string? provider);
+}
diff --git a/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayService.cs b/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayService.cs
index 704749b..63403c6 100644
--- a/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayService.cs
+++ b/src/CMSMicroservice.Application/Common/Interfaces/IPaymentGatewayService.cs
@@ -98,6 +98,26 @@ public class PaymentRequest
/// URL بازگشت بعد از پرداخت
///
public string CallbackUrl { get; set; } = string.Empty;
+
+ ///
+ /// نام پرداختکننده (برای درگاههایی مثل ۲۰کارت)
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// شماره فاکتور / سفارش
+ ///
+ public string Invoice { get; set; } = string.Empty;
+
+ ///
+ /// ایمیل (اختیاری — برای ۲۰کارت)
+ ///
+ public string? Email { get; set; }
+
+ ///
+ /// کد ملی (اختیاری — برای ۲۰کارت)
+ ///
+ public string? NationalId { get; set; }
}
///
diff --git a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommandHandler.cs b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommandHandler.cs
index 42c61ec..576cd6a 100644
--- a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommandHandler.cs
+++ b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyUserPackagePurchasePayment/VerifyUserPackagePurchasePaymentCommandHandler.cs
@@ -8,6 +8,7 @@ using CMSMicroservice.Domain.Entities.Payment;
using CMSMicroservice.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.PackageCQ.Commands.VerifyUserPackagePurchasePayment;
@@ -16,20 +17,23 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
: IRequestHandler
{
private readonly IApplicationDbContext _context;
- private readonly IPaymentGatewayService _paymentGateway;
+ private readonly IPaymentGatewayResolver _paymentGatewayResolver;
+ private readonly IConfiguration _configuration;
private readonly ISender _sender;
private readonly IUserPaymentLock _paymentLock;
private readonly ILogger _logger;
public VerifyUserPackagePurchasePaymentCommandHandler(
IApplicationDbContext context,
- IPaymentGatewayService paymentGateway,
+ IPaymentGatewayResolver paymentGatewayResolver,
+ IConfiguration configuration,
ISender sender,
IUserPaymentLock paymentLock,
ILogger logger)
{
_context = context;
- _paymentGateway = paymentGateway;
+ _paymentGatewayResolver = paymentGatewayResolver;
+ _configuration = configuration;
_sender = sender;
_paymentLock = paymentLock;
_logger = logger;
@@ -105,7 +109,8 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
}
var amountInToman = paymentTx?.Amount ?? purchase.Amount;
- var verifyResult = await _paymentGateway.VerifyPaymentAsync(
+ var gateway = _paymentGatewayResolver.Get(paymentTx?.GatewayProvider);
+ var verifyResult = await gateway.VerifyPaymentAsync(
request.Authority, request.Status, (decimal)amountInToman, ct);
if (!verifyResult.IsSuccess)
@@ -193,17 +198,30 @@ public class VerifyUserPackagePurchasePaymentCommandHandler
await _context.SaveChangesAsync(ct);
}
- var discountMultiplier = purchase.Package?.DiscountMultiplier
+ var packagePrice = purchase.Package?.Price
?? throw new InvalidOperationException("پکیج سفارش یافت نشد");
- var discountAmount = (long)(purchase.Amount * (double)discountMultiplier);
- wallet.Balance += purchase.Amount;
+ var discountMultiplier = purchase.Package.DiscountMultiplier;
+ var discountAmount = (long)(packagePrice * (double)discountMultiplier);
+
+ var isTwentyCard = string.Equals(
+ paymentTx?.GatewayProvider, "20card", StringComparison.OrdinalIgnoreCase);
+ long balanceCredit = purchase.Amount;
+ if (isTwentyCard)
+ {
+ balanceCredit = _configuration.GetValue(
+ $"TwentyCard:Packages:{purchase.PackageId}:WalletCreditToman") ?? 0;
+ if (balanceCredit <= 0)
+ throw new InvalidOperationException("WalletCredit ۲۰کارت برای این پکیج پیکربندی نشده است");
+ }
+
+ wallet.Balance += balanceCredit;
wallet.DiscountBalance += discountAmount;
_context.UserWalletHistories.Add(new UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
- ChangeValue = purchase.Amount,
+ ChangeValue = balanceCredit,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
diff --git a/src/CMSMicroservice.Application/UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommand.cs b/src/CMSMicroservice.Application/UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommand.cs
index 594d105..9aaf337 100644
--- a/src/CMSMicroservice.Application/UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommand.cs
+++ b/src/CMSMicroservice.Application/UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommand.cs
@@ -9,5 +9,7 @@ public record VerifyOtpTokenCommand : IRequest
public string Code { get; init; }
//کد معرف والد
public string? ParentReferralCode { get; init; }
+ //شماره موبایل دعوتکننده (اختیاری)
+ public string? InviterMobile { get; init; }
}
\ No newline at end of file
diff --git a/src/CMSMicroservice.Application/UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommandHandler.cs b/src/CMSMicroservice.Application/UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommandHandler.cs
index 5cd143c..41ce802 100644
--- a/src/CMSMicroservice.Application/UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommandHandler.cs
+++ b/src/CMSMicroservice.Application/UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommandHandler.cs
@@ -118,6 +118,34 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler u.Mobile == inviterMobile, cancellationToken);
+
+ if (inviter == null)
+ {
+ return new VerifyOtpTokenResponseDto
+ {
+ Success = false,
+ Message = "دعوتکننده با این شماره موبایل یافت نشد."
+ };
+ }
+ }
+
// ایجاد کاربر جدید
user = new User
{
@@ -134,6 +162,15 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler
+{
+ 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 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; }
+}
diff --git a/src/CMSMicroservice.Application/UserCQ/Queries/GetInvitationsByFilter/GetInvitationsByFilterQueryHandler.cs b/src/CMSMicroservice.Application/UserCQ/Queries/GetInvitationsByFilter/GetInvitationsByFilterQueryHandler.cs
new file mode 100644
index 0000000..47d0561
--- /dev/null
+++ b/src/CMSMicroservice.Application/UserCQ/Queries/GetInvitationsByFilter/GetInvitationsByFilterQueryHandler.cs
@@ -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
+{
+ private readonly IApplicationDbContext _context;
+
+ public GetInvitationsByFilterQueryHandler(IApplicationDbContext context)
+ {
+ _context = context;
+ }
+
+ public async Task 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
+ };
+ }
+}
diff --git a/src/CMSMicroservice.Application/UserCQ/Queries/GetMyInvitees/GetMyInviteesQuery.cs b/src/CMSMicroservice.Application/UserCQ/Queries/GetMyInvitees/GetMyInviteesQuery.cs
new file mode 100644
index 0000000..9724111
--- /dev/null
+++ b/src/CMSMicroservice.Application/UserCQ/Queries/GetMyInvitees/GetMyInviteesQuery.cs
@@ -0,0 +1,36 @@
+using CMSMicroservice.Application.Common.Models;
+using MediatR;
+
+namespace CMSMicroservice.Application.UserCQ.Queries.GetMyInvitees;
+
+public class GetMyInviteesQuery : IRequest
+{
+ public long UserId { get; set; }
+ public PaginationState? PaginationState { get; set; }
+ /// ALL | ACTIVE | INACTIVE — ClubMembership.IsActive
+ public string? ClubStatusFilter { get; set; }
+}
+
+public class GetMyInviteesResult
+{
+ public MetaData MetaData { get; set; } = new();
+ public List 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; }
+}
diff --git a/src/CMSMicroservice.Application/UserCQ/Queries/GetMyInvitees/GetMyInviteesQueryHandler.cs b/src/CMSMicroservice.Application/UserCQ/Queries/GetMyInvitees/GetMyInviteesQueryHandler.cs
new file mode 100644
index 0000000..a638ff0
--- /dev/null
+++ b/src/CMSMicroservice.Application/UserCQ/Queries/GetMyInvitees/GetMyInviteesQueryHandler.cs
@@ -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
+{
+ private readonly IApplicationDbContext _context;
+ private readonly ICurrentUserService _currentUser;
+
+ public GetMyInviteesQueryHandler(IApplicationDbContext context, ICurrentUserService currentUser)
+ {
+ _context = context;
+ _currentUser = currentUser;
+ }
+
+ public async Task 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()
+ };
+ }
+}
diff --git a/src/CMSMicroservice.Domain/Entities/UserInvitation.cs b/src/CMSMicroservice.Domain/Entities/UserInvitation.cs
new file mode 100644
index 0000000..a6875b5
--- /dev/null
+++ b/src/CMSMicroservice.Domain/Entities/UserInvitation.cs
@@ -0,0 +1,18 @@
+namespace CMSMicroservice.Domain.Entities;
+
+///
+/// رابطه دعوتکننده ↔ دعوتشونده (جدا از معرف شبکه / NetworkParent).
+/// کد دعوت = شماره موبایل دعوتکننده.
+///
+public class UserInvitation : BaseAuditableEntity
+{
+ /// کاربری که دعوت کرده
+ public long InviterUserId { get; set; }
+
+ public virtual User Inviter { get; set; } = null!;
+
+ /// کاربری که دعوت شده (حداکثر یک دعوتکننده)
+ public long InviteeUserId { get; set; }
+
+ public virtual User Invitee { get; set; } = null!;
+}
diff --git a/src/CMSMicroservice.Infrastructure/ConfigureServices.cs b/src/CMSMicroservice.Infrastructure/ConfigureServices.cs
index 8890d70..0f854fb 100644
--- a/src/CMSMicroservice.Infrastructure/ConfigureServices.cs
+++ b/src/CMSMicroservice.Infrastructure/ConfigureServices.cs
@@ -77,27 +77,33 @@ public static class ConfigureServices
});
}
- // Payment Gateway Service - Multi-Provider Architecture
- // پشتیبانی از درگاههای مختلف: ZarinPal, Daya, Mock
+ // Payment Gateway — default provider + multi-gateway resolver (ZarinPal + TwentyCard coexist)
var paymentProvider = configuration.GetValue("PaymentProvider", "Mock")?.ToLowerInvariant();
-
+
+ services.AddHttpClient()
+ .SetHandlerLifetime(TimeSpan.FromMinutes(5));
+ services.AddHttpClient()
+ .SetHandlerLifetime(TimeSpan.FromMinutes(5));
+
switch (paymentProvider)
{
case "zarinpal":
- services.AddHttpClient()
- .SetHandlerLifetime(TimeSpan.FromMinutes(5));
+ services.AddScoped(sp => sp.GetRequiredService());
break;
-
+
case "daya":
- services.AddHttpClient()
+ services.AddHttpClient()
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
+ services.AddScoped(sp => sp.GetRequiredService());
break;
-
+
case "mock":
default:
services.AddScoped();
break;
}
+
+ services.AddScoped();
services.AddScoped(p => p.GetRequiredService());
diff --git a/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs b/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs
index 4216ee8..d206b72 100644
--- a/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs
+++ b/src/CMSMicroservice.Infrastructure/Persistence/ApplicationDbContext.cs
@@ -86,6 +86,7 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
public DbSet Products => Set();
public DbSet ProductImages => Set();
public DbSet Users => Set();
+ public DbSet UserInvitations => Set();
public DbSet OtpTokens => Set();
public DbSet Contracts => Set();
public DbSet UserContracts => Set();
diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserConfiguration.cs b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserConfiguration.cs
index f1710e5..a310ed4 100644
--- a/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserConfiguration.cs
+++ b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserConfiguration.cs
@@ -13,7 +13,7 @@ public class UserConfiguration : IEntityTypeConfiguration
builder.Property(entity => entity.Id).UseIdentityColumn();
builder.Property(entity => entity.FirstName).IsRequired(false);
builder.Property(entity => entity.LastName).IsRequired(false);
- builder.Property(entity => entity.Mobile).IsRequired(true);
+ builder.Property(entity => entity.Mobile).IsRequired(true).HasMaxLength(20);
builder.Property(entity => entity.NationalCode).IsRequired(false);
builder.Property(entity => entity.AvatarPath).IsRequired(false);
builder.Property(entity => entity.ReferralCode).IsRequired(true);
@@ -48,5 +48,10 @@ public class UserConfiguration : IEntityTypeConfiguration
builder.HasIndex(e => e.LegPosition)
.HasDatabaseName("IX_User_LegPosition");
+ // موبایل یکتا برای کاربران فعال (lookup دعوتکننده)
+ builder.HasIndex(e => e.Mobile)
+ .IsUnique()
+ .HasFilter("[IsDeleted] = 0")
+ .HasDatabaseName("IX_User_Mobile_Active");
}
}
diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserInvitationConfiguration.cs b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserInvitationConfiguration.cs
new file mode 100644
index 0000000..34a3945
--- /dev/null
+++ b/src/CMSMicroservice.Infrastructure/Persistence/Configurations/UserInvitationConfiguration.cs
@@ -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
+{
+ public void Configure(EntityTypeBuilder 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");
+ }
+}
diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Migrations/20260920194955_AddUserInvitations.Designer.cs b/src/CMSMicroservice.Infrastructure/Persistence/Migrations/20260920194955_AddUserInvitations.Designer.cs
new file mode 100644
index 0000000..07765c0
--- /dev/null
+++ b/src/CMSMicroservice.Infrastructure/Persistence/Migrations/20260920194955_AddUserInvitations.Designer.cs
@@ -0,0 +1,5269 @@
+//
+using System;
+using CMSMicroservice.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace CMSMicroservice.Infrastructure.Persistence.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20260920194955_AddUserInvitations")]
+ partial class AddUserInvitations
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("CMS")
+ .HasAnnotation("ProductVersion", "9.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogCategory", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("IconName")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(true);
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("LastModified")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("SortOrder")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(0);
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IsActive")
+ .HasDatabaseName("IX_BlogCategories_IsActive");
+
+ b.HasIndex("Slug")
+ .IsUnique()
+ .HasDatabaseName("IX_BlogCategories_Slug");
+
+ b.ToTable("BlogCategories", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogPost", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AuthorUserId")
+ .HasColumnType("bigint");
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("FeaturedImagePath")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("FeaturedImageThumbnailPath")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("HtmlContent")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("IsFeatured")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(false);
+
+ b.Property("LastModified")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PublishedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("ScheduledPublishAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("SortOrder")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(0);
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("Summary")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("ViewCount")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(0);
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorUserId")
+ .HasDatabaseName("IX_BlogPosts_AuthorUserId");
+
+ b.HasIndex("IsFeatured")
+ .HasDatabaseName("IX_BlogPosts_IsFeatured");
+
+ b.HasIndex("PublishedAt")
+ .HasDatabaseName("IX_BlogPosts_PublishedAt");
+
+ b.HasIndex("Slug")
+ .IsUnique()
+ .HasDatabaseName("IX_BlogPosts_Slug");
+
+ b.HasIndex("Status")
+ .HasDatabaseName("IX_BlogPosts_Status");
+
+ b.HasIndex("Status", "PublishedAt")
+ .HasDatabaseName("IX_BlogPosts_Status_PublishedAt");
+
+ b.ToTable("BlogPosts", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogPostCategory", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BlogCategoryId")
+ .HasColumnType("bigint");
+
+ b.Property("BlogPostId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BlogCategoryId");
+
+ b.HasIndex("BlogPostId");
+
+ b.ToTable("BlogPostCategories", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogPostImage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AltText")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("BlogPostId")
+ .HasColumnType("bigint");
+
+ b.Property("Caption")
+ .HasMaxLength(300)
+ .HasColumnType("nvarchar(300)");
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ImagePath")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("LastModified")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SortOrder")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(0);
+
+ b.Property("ThumbnailPath")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BlogPostId");
+
+ b.ToTable("BlogPostImages", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogPostTag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BlogPostId")
+ .HasColumnType("bigint");
+
+ b.Property("TagId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BlogPostId");
+
+ b.HasIndex("TagId");
+
+ b.ToTable("BlogPostTags", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Category", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ImagePath")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("LastModified")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ParentId")
+ .HasColumnType("bigint");
+
+ b.Property("SortOrder")
+ .HasColumnType("int");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.ToTable("Categories", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Club.ClubFeature", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("LastModified")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SortOrder")
+ .HasColumnType("int");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IsActive", "SortOrder")
+ .HasDatabaseName("IX_ClubFeature_IsActive_SortOrder");
+
+ b.ToTable("ClubFeatures", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Club.ClubMembership", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ActivatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("FirstActivationDate")
+ .HasColumnType("datetime2");
+
+ b.Property("FirstPackageId")
+ .HasColumnType("bigint");
+
+ b.Property("GiftValue")
+ .HasColumnType("bigint");
+
+ b.Property("InitialContribution")
+ .HasColumnType("bigint");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("LastActivationDate")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModified")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("LastPackageId")
+ .HasColumnType("bigint");
+
+ b.Property("PurchaseMethod")
+ .HasColumnType("int");
+
+ b.Property("TotalEarned")
+ .HasColumnType("bigint");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FirstPackageId");
+
+ b.HasIndex("IsActive")
+ .HasDatabaseName("IX_ClubMembership_IsActive");
+
+ b.HasIndex("LastActivationDate")
+ .HasDatabaseName("IX_ClubMembership_LastActivationDate");
+
+ b.HasIndex("LastPackageId");
+
+ b.HasIndex("UserId")
+ .IsUnique()
+ .HasDatabaseName("IX_ClubMembership_UserId");
+
+ b.ToTable("ClubMemberships", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Club.ClubMembershipCycle", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClubMembershipId")
+ .HasColumnType("bigint");
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CycleNumber")
+ .HasColumnType("int");
+
+ b.Property("IsCurrentCycle")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(false);
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("LastModified")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("MagicCompletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("MagicStartedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("PackageAmount")
+ .HasColumnType("bigint");
+
+ b.Property("PackageId")
+ .HasColumnType("bigint");
+
+ b.Property("PackagePurchasedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("PurchaseMethod")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClubMembershipId");
+
+ b.HasIndex("PackageId");
+
+ b.HasIndex("PackagePurchasedAt")
+ .HasDatabaseName("IX_ClubMembershipCycle_PackagePurchasedAt");
+
+ b.HasIndex("UserId", "IsCurrentCycle")
+ .HasDatabaseName("IX_ClubMembershipCycle_UserId_IsCurrentCycle");
+
+ b.ToTable("ClubMembershipCycles", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Club.UserClubFeature", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClubFeatureId")
+ .HasColumnType("bigint");
+
+ b.Property("ClubMembershipId")
+ .HasColumnType("bigint");
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("GrantedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(true);
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("LastModified")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Notes")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClubFeatureId");
+
+ b.HasIndex("ClubMembershipId")
+ .HasDatabaseName("IX_UserClubFeature_ClubMembershipId");
+
+ b.HasIndex("UserId", "ClubFeatureId")
+ .IsUnique()
+ .HasDatabaseName("IX_UserClubFeature_UserId_ClubFeatureId");
+
+ b.ToTable("UserClubFeatures", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Commission.UserCommissionPayout", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BalancesEarned")
+ .HasColumnType("int");
+
+ b.Property("BankReferenceId")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("BankTrackingCode")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IbanNumber")
+ .HasMaxLength(26)
+ .HasColumnType("nvarchar(26)");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("LastModified")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PackageId")
+ .HasColumnType("bigint");
+
+ b.Property("PaidAt")
+ .HasColumnType("datetime2");
+
+ b.Property("PaymentFailureReason")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ProcessedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("ProcessedBy")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("RejectionReason")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("TotalAmount")
+ .HasColumnType("bigint");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.Property("ValuePerBalance")
+ .HasColumnType("bigint");
+
+ b.Property("WeekDefinitionId")
+ .HasColumnType("bigint");
+
+ b.Property("WeeklyPoolId")
+ .HasColumnType("bigint");
+
+ b.Property("WithdrawalMethod")
+ .HasColumnType("int");
+
+ b.Property("WithdrawnAt")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PackageId");
+
+ b.HasIndex("Status")
+ .HasDatabaseName("IX_UserCommissionPayout_Status");
+
+ b.HasIndex("WeekDefinitionId");
+
+ b.HasIndex("WeeklyPoolId")
+ .HasDatabaseName("IX_UserCommissionPayout_WeeklyPoolId");
+
+ b.HasIndex("UserId", "WeekDefinitionId", "PackageId")
+ .IsUnique()
+ .HasDatabaseName("IX_UserCommissionPayout_User_WeekDef_Package");
+
+ b.ToTable("UserCommissionPayouts", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Commission.WeeklyCommissionPool", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CalculatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsCalculated")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("LastModified")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PackageId")
+ .HasColumnType("bigint");
+
+ b.Property("TotalBalances")
+ .HasColumnType("int");
+
+ b.Property("TotalPoolAmount")
+ .HasColumnType("bigint");
+
+ b.Property("ValuePerBalance")
+ .HasColumnType("bigint");
+
+ b.Property("WeekDefinitionId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IsCalculated")
+ .HasDatabaseName("IX_WeeklyCommissionPool_IsCalculated");
+
+ b.HasIndex("PackageId");
+
+ b.HasIndex("WeekDefinitionId", "PackageId")
+ .IsUnique()
+ .HasDatabaseName("IX_WeeklyCommissionPool_WeekDef_Package");
+
+ b.ToTable("WeeklyCommissionPools", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Commission.WorkerExecutionLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CompletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Details")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DurationMs")
+ .HasColumnType("bigint");
+
+ b.Property("ErrorCount")
+ .HasColumnType("int");
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(2000)
+ .HasColumnType("nvarchar(2000)");
+
+ b.Property("ErrorStackTrace")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ExecutionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("LastModified")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ProcessedCount")
+ .HasColumnType("int");
+
+ b.Property("StartedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("WeekDefinitionId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("StartedAt");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("WeekDefinitionId");
+
+ b.ToTable("WorkerExecutionLogs", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Configuration.AppVersion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AppName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CurrentVersion")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("LastModified")
+ .HasColumnType("datetime2");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("MinRequiredVersion")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ReleaseNotes")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RequiresFullCacheClear")
+ .HasColumnType("bit");
+
+ b.Property("UpdateMessage")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("AppVersions", "CMS");
+ });
+
+ modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property