feat(user): add user invitation functionality and enhance OTP verification
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:
masoodafar-web
2026-09-20 23:33:03 +03:30
parent f31fb5ee39
commit 2b958bd214
17 changed files with 6019 additions and 3 deletions
@@ -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)