using ColaFlow.Modules.Identity.Application.Services; using ColaFlow.Modules.Identity.Domain.Aggregates.Invitations; using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants; using ColaFlow.Modules.Identity.Domain.Aggregates.Users; using ColaFlow.Modules.Identity.Domain.Repositories; using ColaFlow.Modules.Identity.Domain.Services; using MediatR; using Microsoft.Extensions.Logging; namespace ColaFlow.Modules.Identity.Application.Commands.InviteUser; public class InviteUserCommandHandler( IInvitationRepository invitationRepository, IUserRepository userRepository, IUserTenantRoleRepository userTenantRoleRepository, ITenantRepository tenantRepository, ISecurityTokenService tokenService, IEmailService emailService, IEmailTemplateService templateService, ILogger logger) : IRequestHandler { public async Task Handle(InviteUserCommand request, CancellationToken cancellationToken) { var tenantId = TenantId.Create(request.TenantId); var invitedBy = UserId.Create(request.InvitedBy); // Validate role if (!Enum.TryParse(request.Role, out var role)) throw new ArgumentException($"Invalid role: {request.Role}"); // Check if tenant exists var tenant = await tenantRepository.GetByIdAsync(tenantId, cancellationToken); if (tenant == null) throw new InvalidOperationException($"Tenant {request.TenantId} not found"); // Check if inviter exists var inviter = await userRepository.GetByIdAsync(invitedBy, cancellationToken); if (inviter == null) throw new InvalidOperationException($"Inviter user {request.InvitedBy} not found"); var email = Email.Create(request.Email); // Check if user already exists in this tenant var existingUser = await userRepository.GetByEmailAsync(tenantId, email, cancellationToken); if (existingUser != null) { // Check if user already has a role in this tenant var existingRole = await userTenantRoleRepository.GetByUserAndTenantAsync( UserId.Create(existingUser.Id), tenantId, cancellationToken); if (existingRole != null) throw new InvalidOperationException($"User with email {request.Email} is already a member of this tenant"); } // Check for existing pending invitation var existingInvitation = await invitationRepository.GetPendingByEmailAndTenantAsync( request.Email, tenantId, cancellationToken); if (existingInvitation != null) throw new InvalidOperationException($"A pending invitation already exists for {request.Email} in this tenant"); // Generate secure token var token = tokenService.GenerateToken(); var tokenHash = tokenService.HashToken(token); // Create invitation var invitation = Invitation.Create( tenantId, request.Email, role, tokenHash, invitedBy); await invitationRepository.AddAsync(invitation, cancellationToken); // Send invitation email var invitationLink = $"{request.BaseUrl}/accept-invitation?token={token}"; var htmlBody = templateService.RenderInvitationEmail( recipientName: request.Email.Split('@')[0], // Use email prefix as fallback name tenantName: tenant.Name.Value, inviterName: inviter.FullName.Value, invitationUrl: invitationLink); var emailMessage = new EmailMessage( To: request.Email, Subject: $"You've been invited to join {tenant.Name.Value} on ColaFlow", HtmlBody: htmlBody, PlainTextBody: $"You've been invited to join {tenant.Name.Value}. Click here to accept: {invitationLink}"); var emailSuccess = await emailService.SendEmailAsync(emailMessage, cancellationToken); if (!emailSuccess) { logger.LogWarning( "Failed to send invitation email to {Email} for tenant {TenantId}", request.Email, request.TenantId); } else { logger.LogInformation( "Invitation sent to {Email} for tenant {TenantId} with role {Role}", request.Email, request.TenantId, role); } return invitation.Id; } }