< Summary

Information
Class: Chronicis.Api.Services.ArcService
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/ArcService.cs
Line coverage
100%
Covered lines: 3
Uncovered lines: 0
Coverable lines: 3
Total lines: 271
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/ArcService.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
 9public sealed class ArcService : IArcService
 10{
 11    private readonly ChronicisDbContext _context;
 12    private readonly ILogger<ArcService> _logger;
 13
 14    public ArcService(ChronicisDbContext context, ILogger<ArcService> logger)
 15    {
 2016        _context = context;
 2017        _logger = logger;
 2018    }
 19
 20    /// <summary>
 21    /// Check if user has access to a campaign (via world membership).
 22    /// </summary>
 23    private async Task<bool> UserHasCampaignAccessAsync(Guid campaignId, Guid userId)
 24    {
 25        return await _context.Campaigns
 26            .AnyAsync(c => c.Id == campaignId && c.World.Members.Any(m => m.UserId == userId));
 27    }
 28
 29    /// <summary>
 30    /// Check if user is a GM in the world that owns the campaign.
 31    /// </summary>
 32    private async Task<bool> UserIsCampaignGMAsync(Guid campaignId, Guid userId)
 33    {
 34        return await _context.Campaigns
 35            .AnyAsync(c => c.Id == campaignId && c.World.Members.Any(m => m.UserId == userId && m.Role == WorldRole.GM))
 36    }
 37
 38    /// <summary>
 39    /// Check if user is either the world owner or a GM for the campaign.
 40    /// </summary>
 41    private async Task<bool> UserIsCampaignOwnerOrGMAsync(Guid campaignId, Guid userId)
 42    {
 43        return await _context.Campaigns
 44            .AnyAsync(c => c.Id == campaignId && (c.World.OwnerId == userId
 45                || c.World.Members.Any(m => m.UserId == userId && m.Role == WorldRole.GM)));
 46    }
 47
 48    public async Task<List<ArcDto>> GetArcsByCampaignAsync(Guid campaignId, Guid userId)
 49    {
 50        // Verify user has access to the campaign via world membership
 51        if (!await UserHasCampaignAccessAsync(campaignId, userId))
 52        {
 53            _logger.LogWarningSanitized("Campaign {CampaignId} not found or user {UserId} doesn't have access", campaign
 54            return new List<ArcDto>();
 55        }
 56
 57        return await _context.Arcs
 58            .AsNoTracking()
 59            .Where(a => a.CampaignId == campaignId)
 60            .OrderBy(a => a.SortOrder)
 61            .ThenBy(a => a.CreatedAt)
 62            .Select(a => new ArcDto
 63            {
 64                Id = a.Id,
 65                CampaignId = a.CampaignId,
 66                Name = a.Name,
 67                Description = a.Description,
 68                PrivateNotes = null,
 69                SortOrder = a.SortOrder,
 70                SessionCount = _context.Articles.Count(art => art.ArcId == a.Id),
 71                IsActive = a.IsActive,
 72                CreatedAt = a.CreatedAt,
 73                CreatedBy = a.CreatedBy,
 74                CreatedByName = a.Creator.DisplayName
 75            })
 76            .ToListAsync();
 77    }
 78
 79    public async Task<ArcDto?> GetArcAsync(Guid arcId, Guid userId)
 80    {
 81        return await _context.Arcs
 82            .AsNoTracking()
 83            .Where(a => a.Id == arcId && a.Campaign.World.Members.Any(m => m.UserId == userId))
 84            .Select(a => new ArcDto
 85            {
 86                Id = a.Id,
 87                CampaignId = a.CampaignId,
 88                Name = a.Name,
 89                Description = a.Description,
 90                PrivateNotes = a.Campaign.World.OwnerId == userId
 91                    || a.Campaign.World.Members.Any(m => m.UserId == userId && m.Role == WorldRole.GM)
 92                    ? a.PrivateNotes
 93                    : null,
 94                SortOrder = a.SortOrder,
 95                SessionCount = _context.Articles.Count(art => art.ArcId == a.Id),
 96                IsActive = a.IsActive,
 97                CreatedAt = a.CreatedAt,
 98                CreatedBy = a.CreatedBy,
 99                CreatedByName = a.Creator.DisplayName
 100            })
 101            .FirstOrDefaultAsync();
 102    }
 103
 104    public async Task<ArcDto?> CreateArcAsync(ArcCreateDto dto, Guid userId)
 105    {
 106        // Only GMs can create arcs
 107        if (!await UserIsCampaignGMAsync(dto.CampaignId, userId))
 108        {
 109            _logger.LogWarningSanitized("Campaign {CampaignId} not found or user {UserId} is not a GM", dto.CampaignId, 
 110            return null;
 111        }
 112
 113        // Use provided sort order or auto-calculate next
 114        var sortOrder = dto.SortOrder;
 115        if (sortOrder == 0)
 116        {
 117            var maxSortOrder = await _context.Arcs
 118                .Where(a => a.CampaignId == dto.CampaignId)
 119                .MaxAsync(a => (int?)a.SortOrder) ?? 0;
 120            sortOrder = maxSortOrder + 1;
 121        }
 122
 123        var arc = new Arc
 124        {
 125            Id = Guid.NewGuid(),
 126            CampaignId = dto.CampaignId,
 127            Name = dto.Name,
 128            Description = dto.Description,
 129            SortOrder = sortOrder,
 130            CreatedAt = DateTime.UtcNow,
 131            CreatedBy = userId
 132        };
 133
 134        _context.Arcs.Add(arc);
 135        await _context.SaveChangesAsync();
 136
 137        _logger.LogTraceSanitized("Created arc {ArcId} '{ArcName}' in campaign {CampaignId}",
 138            arc.Id, arc.Name, arc.CampaignId);
 139
 140        return new ArcDto
 141        {
 142            Id = arc.Id,
 143            CampaignId = arc.CampaignId,
 144            Name = arc.Name,
 145            Description = arc.Description,
 146            PrivateNotes = null,
 147            SortOrder = arc.SortOrder,
 148            SessionCount = 0,
 149            IsActive = arc.IsActive,
 150            CreatedAt = arc.CreatedAt,
 151            CreatedBy = arc.CreatedBy
 152        };
 153    }
 154
 155    public async Task<ArcDto?> UpdateArcAsync(Guid arcId, ArcUpdateDto dto, Guid userId)
 156    {
 157        var arc = await _context.Arcs
 158            .Include(a => a.Creator)
 159            .FirstOrDefaultAsync(a => a.Id == arcId);
 160
 161        if (arc == null)
 162        {
 163            _logger.LogWarningSanitized("Arc {ArcId} not found", arcId);
 164            return null;
 165        }
 166
 167        if (!await UserIsCampaignOwnerOrGMAsync(arc.CampaignId, userId))
 168        {
 169            _logger.LogWarningSanitized("User {UserId} is not authorized to update arc {ArcId}", userId, arcId);
 170            return null;
 171        }
 172
 173        arc.Name = dto.Name;
 174        arc.Description = dto.Description;
 175        arc.PrivateNotes = string.IsNullOrWhiteSpace(dto.PrivateNotes) ? null : dto.PrivateNotes;
 176
 177        if (dto.SortOrder.HasValue)
 178        {
 179            arc.SortOrder = dto.SortOrder.Value;
 180        }
 181
 182        await _context.SaveChangesAsync();
 183
 184        _logger.LogTraceSanitized("Updated arc {ArcId} '{ArcName}'", arc.Id, arc.Name);
 185
 186        var sessionCount = await _context.Articles.CountAsync(a => a.ArcId == arcId);
 187
 188        return new ArcDto
 189        {
 190            Id = arc.Id,
 191            CampaignId = arc.CampaignId,
 192            Name = arc.Name,
 193            Description = arc.Description,
 194            PrivateNotes = arc.PrivateNotes,
 195            SortOrder = arc.SortOrder,
 196            SessionCount = sessionCount,
 197            IsActive = arc.IsActive,
 198            CreatedAt = arc.CreatedAt,
 199            CreatedBy = arc.CreatedBy,
 200            CreatedByName = arc.Creator.DisplayName
 201        };
 202    }
 203
 204    public async Task<bool> DeleteArcAsync(Guid arcId, Guid userId)
 205    {
 206        var arc = await _context.Arcs
 207            .FirstOrDefaultAsync(a => a.Id == arcId);
 208
 209        if (arc == null)
 210        {
 211            _logger.LogWarningSanitized("Arc {ArcId} not found", arcId);
 212            return false;
 213        }
 214
 215        if (!await UserIsCampaignGMAsync(arc.CampaignId, userId))
 216        {
 217            _logger.LogWarningSanitized("User {UserId} is not a GM for arc {ArcId}", userId, arcId);
 218            return false;
 219        }
 220
 221        // Check if arc has sessions
 222        var hasContent = await _context.Articles.AnyAsync(a => a.ArcId == arcId);
 223        if (hasContent)
 224        {
 225            _logger.LogWarningSanitized("Cannot delete arc {ArcId} - it has sessions", arcId);
 226            return false;
 227        }
 228
 229        _context.Arcs.Remove(arc);
 230        await _context.SaveChangesAsync();
 231
 232        _logger.LogTraceSanitized("Deleted arc {ArcId}", arcId);
 233        return true;
 234    }
 235
 236    public async Task<bool> ActivateArcAsync(Guid arcId, Guid userId)
 237    {
 238        var arc = await _context.Arcs
 239            .Include(a => a.Campaign)
 240                .ThenInclude(c => c.World)
 241                    .ThenInclude(w => w.Members)
 242            .FirstOrDefaultAsync(a => a.Id == arcId);
 243
 244        if (arc == null)
 245            return false;
 246
 247        // Only the world owner or GMs can activate arcs
 248        if (!(arc.Campaign.World.OwnerId == userId
 249            || arc.Campaign.World.Members.Any(m => m.UserId == userId && m.Role == WorldRole.GM)))
 250            return false;
 251
 252        // Deactivate all arcs in the same campaign
 253        var campaignArcs = await _context.Arcs
 254            .Where(a => a.CampaignId == arc.CampaignId && a.IsActive)
 255            .ToListAsync();
 256
 257        foreach (var a in campaignArcs)
 258        {
 259            a.IsActive = false;
 260        }
 261
 262        // Activate this arc
 263        arc.IsActive = true;
 264
 265        await _context.SaveChangesAsync();
 266
 267        _logger.LogTraceSanitized("Activated arc {ArcId} in campaign {CampaignId}", arcId, arc.CampaignId);
 268
 269        return true;
 270    }
 271}