< Summary

Information
Class: Chronicis.Api.Services.WorldInvitationService
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/WorldInvitationService.cs
Line coverage
100%
Covered lines: 3
Uncovered lines: 0
Coverable lines: 3
Total lines: 227
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/WorldInvitationService.cs

#LineLine coverage
 1using Chronicis.Api.Data;
 2using Chronicis.Shared.DTOs;
 3using Chronicis.Shared.Enums;
 4using Chronicis.Shared.Models;
 5using Microsoft.EntityFrameworkCore;
 6
 7namespace Chronicis.Api.Services;
 8
 9/// <summary>
 10/// Service for world invitation management
 11/// </summary>
 12public sealed class WorldInvitationService : IWorldInvitationService
 13{
 14    private readonly ChronicisDbContext _context;
 15    private readonly ILogger<WorldInvitationService> _logger;
 16
 17    public WorldInvitationService(ChronicisDbContext context, ILogger<WorldInvitationService> logger)
 18    {
 1819        _context = context;
 1820        _logger = logger;
 1821    }
 22
 23    public async Task<List<WorldInvitationDto>> GetInvitationsAsync(Guid worldId, Guid userId)
 24    {
 25        // Only GMs can view invitations
 26        var isGM = await _context.WorldMembers
 27            .AnyAsync(m => m.WorldId == worldId && m.UserId == userId && m.Role == WorldRole.GM);
 28
 29        if (!isGM)
 30            return new List<WorldInvitationDto>();
 31
 32        var invitations = await _context.WorldInvitations
 33            .Include(i => i.Creator)
 34            .Where(i => i.WorldId == worldId)
 35            .OrderByDescending(i => i.CreatedAt)
 36            .ToListAsync();
 37
 38        return invitations.Select(i => new WorldInvitationDto
 39        {
 40            Id = i.Id,
 41            WorldId = i.WorldId,
 42            Code = i.Code,
 43            Role = i.Role,
 44            CreatedBy = i.CreatedBy,
 45            CreatorName = i.Creator?.DisplayName ?? "Unknown",
 46            CreatedAt = i.CreatedAt,
 47            ExpiresAt = i.ExpiresAt,
 48            MaxUses = i.MaxUses,
 49            UsedCount = i.UsedCount,
 50            IsActive = i.IsActive
 51        }).ToList();
 52    }
 53
 54    public async Task<WorldInvitationDto?> CreateInvitationAsync(Guid worldId, WorldInvitationCreateDto dto, Guid userId
 55    {
 56        // Only GMs can create invitations
 57        var isGM = await _context.WorldMembers
 58            .AnyAsync(m => m.WorldId == worldId && m.UserId == userId && m.Role == WorldRole.GM);
 59
 60        if (!isGM)
 61            return null;
 62
 63        // Generate unique code
 64        string code;
 65        int attempts = 0;
 66        do
 67        {
 68            code = Utilities.InvitationCodeGenerator.GenerateCode();
 69            attempts++;
 70        } while (await _context.WorldInvitations.AnyAsync(i => i.Code == code) && attempts < 10);
 71
 72        if (attempts >= 10)
 73        {
 74            _logger.LogErrorSanitized("Failed to generate unique invitation code after 10 attempts");
 75            return null;
 76        }
 77
 78        var invitation = new WorldInvitation
 79        {
 80            Id = Guid.NewGuid(),
 81            WorldId = worldId,
 82            Code = code,
 83            Role = dto.Role,
 84            CreatedBy = userId,
 85            CreatedAt = DateTime.UtcNow,
 86            ExpiresAt = dto.ExpiresAt,
 87            MaxUses = dto.MaxUses,
 88            UsedCount = 0,
 89            IsActive = true
 90        };
 91
 92        _context.WorldInvitations.Add(invitation);
 93        await _context.SaveChangesAsync();
 94
 95        _logger.LogTraceSanitized("Created invitation {Code} for world {WorldId} by user {UserId}",
 96            code, worldId, userId);
 97
 98        var creator = await _context.Users.FindAsync(userId);
 99
 100        return new WorldInvitationDto
 101        {
 102            Id = invitation.Id,
 103            WorldId = invitation.WorldId,
 104            Code = invitation.Code,
 105            Role = invitation.Role,
 106            CreatedBy = invitation.CreatedBy,
 107            CreatorName = creator?.DisplayName ?? "Unknown",
 108            CreatedAt = invitation.CreatedAt,
 109            ExpiresAt = invitation.ExpiresAt,
 110            MaxUses = invitation.MaxUses,
 111            UsedCount = invitation.UsedCount,
 112            IsActive = invitation.IsActive
 113        };
 114    }
 115
 116    public async Task<bool> RevokeInvitationAsync(Guid worldId, Guid invitationId, Guid userId)
 117    {
 118        // Only GMs can revoke invitations
 119        var isGM = await _context.WorldMembers
 120            .AnyAsync(m => m.WorldId == worldId && m.UserId == userId && m.Role == WorldRole.GM);
 121
 122        if (!isGM)
 123            return false;
 124
 125        var invitation = await _context.WorldInvitations
 126            .FirstOrDefaultAsync(i => i.Id == invitationId && i.WorldId == worldId);
 127
 128        if (invitation == null)
 129            return false;
 130
 131        invitation.IsActive = false;
 132        await _context.SaveChangesAsync();
 133
 134        _logger.LogTraceSanitized("Revoked invitation {InvitationId} for world {WorldId}", invitationId, worldId);
 135
 136        return true;
 137    }
 138
 139    public async Task<WorldJoinResultDto> JoinWorldAsync(string code, Guid userId)
 140    {
 141        var normalizedCode = Utilities.InvitationCodeGenerator.NormalizeCode(code);
 142
 143        if (!Utilities.InvitationCodeGenerator.IsValidFormat(normalizedCode))
 144        {
 145            return new WorldJoinResultDto
 146            {
 147                Success = false,
 148                ErrorMessage = "Invalid invitation code format"
 149            };
 150        }
 151
 152        var invitation = await _context.WorldInvitations
 153            .Include(i => i.World)
 154            .FirstOrDefaultAsync(i => i.Code == normalizedCode && i.IsActive);
 155
 156        if (invitation == null)
 157        {
 158            return new WorldJoinResultDto
 159            {
 160                Success = false,
 161                ErrorMessage = "Invitation not found or has been revoked"
 162            };
 163        }
 164
 165        // Check expiration
 166        if (invitation.ExpiresAt.HasValue && invitation.ExpiresAt.Value < DateTime.UtcNow)
 167        {
 168            return new WorldJoinResultDto
 169            {
 170                Success = false,
 171                ErrorMessage = "This invitation has expired"
 172            };
 173        }
 174
 175        // Check max uses
 176        if (invitation.MaxUses.HasValue && invitation.UsedCount >= invitation.MaxUses.Value)
 177        {
 178            return new WorldJoinResultDto
 179            {
 180                Success = false,
 181                ErrorMessage = "This invitation has reached its maximum number of uses"
 182            };
 183        }
 184
 185        // Check if user is already a member
 186        var existingMember = await _context.WorldMembers
 187            .FirstOrDefaultAsync(m => m.WorldId == invitation.WorldId && m.UserId == userId);
 188
 189        if (existingMember != null)
 190        {
 191            return new WorldJoinResultDto
 192            {
 193                Success = false,
 194                ErrorMessage = "You are already a member of this world"
 195            };
 196        }
 197
 198        // Create membership
 199        var member = new WorldMember
 200        {
 201            Id = Guid.NewGuid(),
 202            WorldId = invitation.WorldId,
 203            UserId = userId,
 204            Role = invitation.Role,
 205            JoinedAt = DateTime.UtcNow,
 206            InvitedBy = invitation.CreatedBy
 207        };
 208
 209        _context.WorldMembers.Add(member);
 210
 211        // Increment usage count
 212        invitation.UsedCount++;
 213
 214        await _context.SaveChangesAsync();
 215
 216        _logger.LogTraceSanitized("User {UserId} joined world {WorldId} via invitation {Code}",
 217            userId, invitation.WorldId, normalizedCode);
 218
 219        return new WorldJoinResultDto
 220        {
 221            Success = true,
 222            WorldId = invitation.WorldId,
 223            WorldName = invitation.World?.Name,
 224            AssignedRole = invitation.Role
 225        };
 226    }
 227}