< 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: 144
Uncovered lines: 0
Coverable lines: 144
Total lines: 230
Line coverage: 100%
Branch coverage
100%
Covered branches: 18
Total branches: 18
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
UserHasCampaignAccessAsync()100%11100%
GetArcsByCampaignAsync()100%22100%
GetArcAsync()100%11100%
CreateArcAsync()100%44100%
UpdateArcAsync()100%44100%
DeleteArcAsync()100%44100%
ActivateArcAsync()100%66100%

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.Models;
 4using Microsoft.EntityFrameworkCore;
 5
 6namespace Chronicis.Api.Services;
 7
 8public class ArcService : IArcService
 9{
 10    private readonly ChronicisDbContext _context;
 11    private readonly ILogger<ArcService> _logger;
 12
 2013    public ArcService(ChronicisDbContext context, ILogger<ArcService> logger)
 14    {
 2015        _context = context;
 2016        _logger = logger;
 2017    }
 18
 19    /// <summary>
 20    /// Check if user has access to a campaign (via world membership).
 21    /// </summary>
 22    private async Task<bool> UserHasCampaignAccessAsync(Guid campaignId, Guid userId)
 23    {
 624        return await _context.Campaigns
 625            .AnyAsync(c => c.Id == campaignId && c.World.Members.Any(m => m.UserId == userId));
 626    }
 27
 28    public async Task<List<ArcDto>> GetArcsByCampaignAsync(Guid campaignId, Guid userId)
 29    {
 30        // Verify user has access to the campaign via world membership
 331        if (!await UserHasCampaignAccessAsync(campaignId, userId))
 32        {
 133            _logger.LogWarning("Campaign {CampaignId} not found or user {UserId} doesn't have access", campaignId, userI
 134            return new List<ArcDto>();
 35        }
 36
 237        return await _context.Arcs
 238            .AsNoTracking()
 239            .Where(a => a.CampaignId == campaignId)
 240            .OrderBy(a => a.SortOrder)
 241            .ThenBy(a => a.CreatedAt)
 242            .Select(a => new ArcDto
 243            {
 244                Id = a.Id,
 245                CampaignId = a.CampaignId,
 246                Name = a.Name,
 247                Description = a.Description,
 248                SortOrder = a.SortOrder,
 249                SessionCount = _context.Articles.Count(art => art.ArcId == a.Id),
 250                IsActive = a.IsActive,
 251                CreatedAt = a.CreatedAt,
 252                CreatedBy = a.CreatedBy,
 253                CreatedByName = a.Creator.DisplayName
 254            })
 255            .ToListAsync();
 356    }
 57
 58    public async Task<ArcDto?> GetArcAsync(Guid arcId, Guid userId)
 59    {
 360        return await _context.Arcs
 361            .AsNoTracking()
 362            .Where(a => a.Id == arcId && a.Campaign.World.Members.Any(m => m.UserId == userId))
 363            .Select(a => new ArcDto
 364            {
 365                Id = a.Id,
 366                CampaignId = a.CampaignId,
 367                Name = a.Name,
 368                Description = a.Description,
 369                SortOrder = a.SortOrder,
 370                SessionCount = _context.Articles.Count(art => art.ArcId == a.Id),
 371                IsActive = a.IsActive,
 372                CreatedAt = a.CreatedAt,
 373                CreatedBy = a.CreatedBy,
 374                CreatedByName = a.Creator.DisplayName
 375            })
 376            .FirstOrDefaultAsync();
 377    }
 78
 79    public async Task<ArcDto?> CreateArcAsync(ArcCreateDto dto, Guid userId)
 80    {
 81        // Verify user has access to the campaign via world membership
 382        if (!await UserHasCampaignAccessAsync(dto.CampaignId, userId))
 83        {
 184            _logger.LogWarning("Campaign {CampaignId} not found or user {UserId} doesn't have access", dto.CampaignId, u
 185            return null;
 86        }
 87
 88        // Use provided sort order or auto-calculate next
 289        var sortOrder = dto.SortOrder;
 290        if (sortOrder == 0)
 91        {
 192            var maxSortOrder = await _context.Arcs
 193                .Where(a => a.CampaignId == dto.CampaignId)
 194                .MaxAsync(a => (int?)a.SortOrder) ?? 0;
 195            sortOrder = maxSortOrder + 1;
 96        }
 97
 298        var arc = new Arc
 299        {
 2100            Id = Guid.NewGuid(),
 2101            CampaignId = dto.CampaignId,
 2102            Name = dto.Name,
 2103            Description = dto.Description,
 2104            SortOrder = sortOrder,
 2105            CreatedAt = DateTime.UtcNow,
 2106            CreatedBy = userId
 2107        };
 108
 2109        _context.Arcs.Add(arc);
 2110        await _context.SaveChangesAsync();
 111
 2112        _logger.LogDebug("Created arc {ArcId} '{ArcName}' in campaign {CampaignId}",
 2113            arc.Id, arc.Name, arc.CampaignId);
 114
 2115        return new ArcDto
 2116        {
 2117            Id = arc.Id,
 2118            CampaignId = arc.CampaignId,
 2119            Name = arc.Name,
 2120            Description = arc.Description,
 2121            SortOrder = arc.SortOrder,
 2122            SessionCount = 0,
 2123            IsActive = arc.IsActive,
 2124            CreatedAt = arc.CreatedAt,
 2125            CreatedBy = arc.CreatedBy
 2126        };
 3127    }
 128
 129    public async Task<ArcDto?> UpdateArcAsync(Guid arcId, ArcUpdateDto dto, Guid userId)
 130    {
 3131        var arc = await _context.Arcs
 3132            .Include(a => a.Creator)
 3133            .FirstOrDefaultAsync(a => a.Id == arcId && a.Campaign.World.Members.Any(m => m.UserId == userId));
 134
 3135        if (arc == null)
 136        {
 2137            _logger.LogWarning("Arc {ArcId} not found or user {UserId} doesn't have access", arcId, userId);
 2138            return null;
 139        }
 140
 1141        arc.Name = dto.Name;
 1142        arc.Description = dto.Description;
 143
 1144        if (dto.SortOrder.HasValue)
 145        {
 1146            arc.SortOrder = dto.SortOrder.Value;
 147        }
 148
 1149        await _context.SaveChangesAsync();
 150
 1151        _logger.LogDebug("Updated arc {ArcId} '{ArcName}'", arc.Id, arc.Name);
 152
 1153        var sessionCount = await _context.Articles.CountAsync(a => a.ArcId == arcId);
 154
 1155        return new ArcDto
 1156        {
 1157            Id = arc.Id,
 1158            CampaignId = arc.CampaignId,
 1159            Name = arc.Name,
 1160            Description = arc.Description,
 1161            SortOrder = arc.SortOrder,
 1162            SessionCount = sessionCount,
 1163            IsActive = arc.IsActive,
 1164            CreatedAt = arc.CreatedAt,
 1165            CreatedBy = arc.CreatedBy,
 1166            CreatedByName = arc.Creator.DisplayName
 1167        };
 3168    }
 169
 170    public async Task<bool> DeleteArcAsync(Guid arcId, Guid userId)
 171    {
 4172        var arc = await _context.Arcs
 4173            .FirstOrDefaultAsync(a => a.Id == arcId && a.Campaign.World.Members.Any(m => m.UserId == userId));
 174
 4175        if (arc == null)
 176        {
 2177            _logger.LogWarning("Arc {ArcId} not found or user {UserId} doesn't have access", arcId, userId);
 2178            return false;
 179        }
 180
 181        // Check if arc has sessions
 2182        var hasContent = await _context.Articles.AnyAsync(a => a.ArcId == arcId);
 2183        if (hasContent)
 184        {
 1185            _logger.LogWarning("Cannot delete arc {ArcId} - it has sessions", arcId);
 1186            return false;
 187        }
 188
 1189        _context.Arcs.Remove(arc);
 1190        await _context.SaveChangesAsync();
 191
 1192        _logger.LogDebug("Deleted arc {ArcId}", arcId);
 1193        return true;
 4194    }
 195
 196    public async Task<bool> ActivateArcAsync(Guid arcId, Guid userId)
 197    {
 4198        var arc = await _context.Arcs
 4199            .Include(a => a.Campaign)
 4200                .ThenInclude(c => c.World)
 4201                    .ThenInclude(w => w.Members)
 4202            .FirstOrDefaultAsync(a => a.Id == arcId);
 203
 4204        if (arc == null)
 1205            return false;
 206
 207        // User must be a member of the world to activate arcs
 7208        if (!arc.Campaign.World.Members.Any(m => m.UserId == userId))
 1209            return false;
 210
 211        // Deactivate all arcs in the same campaign
 2212        var campaignArcs = await _context.Arcs
 2213            .Where(a => a.CampaignId == arc.CampaignId && a.IsActive)
 2214            .ToListAsync();
 215
 6216        foreach (var a in campaignArcs)
 217        {
 1218            a.IsActive = false;
 219        }
 220
 221        // Activate this arc
 2222        arc.IsActive = true;
 223
 2224        await _context.SaveChangesAsync();
 225
 2226        _logger.LogDebug("Activated arc {ArcId} in campaign {CampaignId}", arcId, arc.CampaignId);
 227
 2228        return true;
 4229    }
 230}