feat(user): add user invitation functionality and enhance OTP verification
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m27s
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m27s
- Introduced UserInvitation entity and updated IApplicationDbContext to include UserInvitations DbSet. - Enhanced VerifyOtpTokenCommand and its handler to support optional inviter mobile number for user registration. - Added new Protobuf definitions for GetMyInvitees and GetInvitationsByFilter RPCs to manage user invitations. - Updated UserService to implement methods for retrieving invitees and invitations based on filters. - Bumped Protobuf project version to reflect the addition of new features.
This commit is contained in:
@@ -21,6 +21,7 @@ public interface IApplicationDbContext
|
||||
DbSet<Product> Products { get; }
|
||||
DbSet<ProductImage> ProductImages { get; }
|
||||
DbSet<User> Users { get; }
|
||||
DbSet<UserInvitation> UserInvitations { get; }
|
||||
DbSet<OtpToken> OtpTokens { get; }
|
||||
DbSet<Contract> Contracts { get; }
|
||||
DbSet<UserContract> UserContracts { get; }
|
||||
|
||||
+2
@@ -9,5 +9,7 @@ public record VerifyOtpTokenCommand : IRequest<VerifyOtpTokenResponseDto>
|
||||
public string Code { get; init; }
|
||||
//کد معرف والد
|
||||
public string? ParentReferralCode { get; init; }
|
||||
//شماره موبایل دعوتکننده (اختیاری)
|
||||
public string? InviterMobile { get; init; }
|
||||
|
||||
}
|
||||
+37
@@ -118,6 +118,34 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
|
||||
else
|
||||
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
|
||||
{
|
||||
@@ -134,6 +162,15 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
|
||||
user.AddDomainEvent(new CreateNewUserEvent(user));
|
||||
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 };
|
||||
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!;
|
||||
}
|
||||
@@ -86,6 +86,7 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
|
||||
public DbSet<Product> Products => Set<Product>();
|
||||
public DbSet<ProductImage> ProductImages => Set<ProductImage>();
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<UserInvitation> UserInvitations => Set<UserInvitation>();
|
||||
public DbSet<OtpToken> OtpTokens => Set<OtpToken>();
|
||||
public DbSet<Contract> Contracts => Set<Contract>();
|
||||
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.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<User>
|
||||
builder.HasIndex(e => e.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")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<DateTime?>("MobileVerifiedAt")
|
||||
.HasColumnType("datetime2");
|
||||
@@ -3606,6 +3606,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("LegPosition")
|
||||
.HasDatabaseName("IX_User_LegPosition");
|
||||
|
||||
b.HasIndex("Mobile")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_User_Mobile_Active")
|
||||
.HasFilter("[IsDeleted] = 0");
|
||||
|
||||
b.HasIndex("NetworkParentId")
|
||||
.HasDatabaseName("IX_User_NetworkParentId");
|
||||
|
||||
@@ -3750,6 +3755,47 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -4871,6 +4917,25 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>0.0.212</Version>
|
||||
<Version>0.0.213</Version>
|
||||
<DebugType>None</DebugType>
|
||||
<DebugSymbols>False</DebugSymbols>
|
||||
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||
|
||||
@@ -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){
|
||||
option (google.api.http) = {
|
||||
post: "/Customer/UploadAvatar"
|
||||
@@ -294,6 +304,8 @@ message VerifyOtpTokenRequest
|
||||
string purpose = 2;
|
||||
string code = 3;
|
||||
google.protobuf.StringValue parent_referral_code = 4;
|
||||
// شماره موبایل دعوتکننده (اختیاری؛ فقط برای ثبتنام جدید)
|
||||
google.protobuf.StringValue inviter_mobile = 5;
|
||||
}
|
||||
message VerifyOtpTokenResponse
|
||||
{
|
||||
@@ -466,3 +478,78 @@ message UpdateCustomerSettingsRequest
|
||||
google.protobuf.StringValue time_zone = 6;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,11 @@ using CMSMicroservice.Application.UserCQ.Commands.VerifyOtpToken;
|
||||
using CMSMicroservice.Application.UserCQ.Commands.AcceptContract;
|
||||
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerProfile;
|
||||
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.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Authorization;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Grpc.Core;
|
||||
@@ -312,6 +315,117 @@ public class UserService : UserContract.UserContractBase
|
||||
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)
|
||||
{
|
||||
if (request.FileData == null || request.FileData.Length == 0)
|
||||
|
||||
Reference in New Issue
Block a user