< Summary

Information
Class: Chronicis.Api.Services.CampaignService
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/CampaignService.cs
Line coverage
96%
Covered lines: 165
Uncovered lines: 6
Coverable lines: 171
Total lines: 284
Line coverage: 96.4%
Branch coverage
74%
Covered branches: 40
Total branches: 54
Branch coverage: 74%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetCampaignAsync()100%44100%
CreateCampaignAsync()83.33%6697.43%
UpdateCampaignAsync()100%44100%
GetUserRoleAsync()75%4485.71%
UserHasAccessAsync()50%2283.33%
UserIsGMAsync()50%2287.5%
ActivateCampaignAsync()100%66100%
GetActiveContextAsync()80%101092.3%
MapToDto(...)50%66100%
MapToDetailDto(...)50%1010100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/CampaignService.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 campaign management
 11/// </summary>
 12public class CampaignService : ICampaignService
 13{
 14    private readonly ChronicisDbContext _context;
 15    private readonly ILogger<CampaignService> _logger;
 16
 2517    public CampaignService(ChronicisDbContext context, ILogger<CampaignService> logger)
 18    {
 2519        _context = context;
 2520        _logger = logger;
 2521    }
 22
 23    public async Task<CampaignDetailDto?> GetCampaignAsync(Guid campaignId, Guid userId)
 24    {
 325        var campaign = await _context.Campaigns
 326            .Include(c => c.Owner)
 327            .Include(c => c.Arcs)
 328            .FirstOrDefaultAsync(c => c.Id == campaignId);
 29
 330        if (campaign == null)
 131            return null;
 32
 33        // Check access via world membership
 234        if (!await UserHasAccessAsync(campaignId, userId))
 135            return null;
 36
 137        return MapToDetailDto(campaign);
 338    }
 39
 40    public async Task<CampaignDto> CreateCampaignAsync(CampaignCreateDto dto, Guid userId)
 41    {
 42        // Verify user is GM in the world
 443        var world = await _context.Worlds.FindAsync(dto.WorldId);
 444        if (world == null)
 145            throw new InvalidOperationException("World not found");
 46
 347        var isGM = await _context.WorldMembers
 348            .AnyAsync(wm => wm.WorldId == dto.WorldId && wm.UserId == userId && wm.Role == WorldRole.GM);
 49
 350        if (!isGM)
 151            throw new UnauthorizedAccessException("Only GMs can create campaigns");
 52
 253        var user = await _context.Users.FindAsync(userId);
 254        if (user == null)
 055            throw new InvalidOperationException("User not found");
 56
 257        _logger.LogDebug("Creating campaign '{Name}' in world {WorldId} for user {UserId}",
 258            dto.Name, dto.WorldId, userId);
 59
 60        // Create the Campaign entity
 261        var campaign = new Campaign
 262        {
 263            Id = Guid.NewGuid(),
 264            WorldId = dto.WorldId,
 265            Name = dto.Name,
 266            Description = dto.Description,
 267            OwnerId = userId,
 268            CreatedAt = DateTime.UtcNow
 269        };
 270        _context.Campaigns.Add(campaign);
 71
 72        // Create a default Arc (Act 1)
 273        var defaultArc = new Arc
 274        {
 275            Id = Guid.NewGuid(),
 276            CampaignId = campaign.Id,
 277            Name = "Act 1",
 278            Description = null,
 279            SortOrder = 1,
 280            CreatedAt = DateTime.UtcNow,
 281            CreatedBy = userId
 282        };
 283        _context.Arcs.Add(defaultArc);
 84
 285        await _context.SaveChangesAsync();
 86
 287        _logger.LogDebug("Created campaign {CampaignId} with default Arc for user {UserId}",
 288            campaign.Id, userId);
 89
 90        // Return DTO
 291        campaign.Owner = user;
 292        return MapToDto(campaign);
 293    }
 94
 95    public async Task<CampaignDto?> UpdateCampaignAsync(Guid campaignId, CampaignUpdateDto dto, Guid userId)
 96    {
 397        var campaign = await _context.Campaigns
 398            .Include(c => c.Owner)
 399            .FirstOrDefaultAsync(c => c.Id == campaignId);
 100
 3101        if (campaign == null)
 1102            return null;
 103
 104        // Only GM can update
 2105        if (!await UserIsGMAsync(campaignId, userId))
 1106            return null;
 107
 1108        campaign.Name = dto.Name;
 1109        campaign.Description = dto.Description;
 1110        campaign.StartedAt = dto.StartedAt;
 1111        campaign.EndedAt = dto.EndedAt;
 112
 1113        await _context.SaveChangesAsync();
 114
 1115        _logger.LogDebug("Updated campaign {CampaignId}", campaignId);
 116
 1117        return MapToDto(campaign);
 3118    }
 119
 120    public async Task<WorldRole?> GetUserRoleAsync(Guid campaignId, Guid userId)
 121    {
 122        // Get the world for this campaign, then check world membership
 3123        var campaign = await _context.Campaigns.FindAsync(campaignId);
 3124        if (campaign == null)
 0125            return null;
 126
 3127        var member = await _context.WorldMembers
 3128            .FirstOrDefaultAsync(wm => wm.WorldId == campaign.WorldId && wm.UserId == userId);
 129
 3130        return member?.Role;
 3131    }
 132
 133    public async Task<bool> UserHasAccessAsync(Guid campaignId, Guid userId)
 134    {
 135        // User has access if they're a member of the campaign's world
 4136        var campaign = await _context.Campaigns.FindAsync(campaignId);
 4137        if (campaign == null)
 0138            return false;
 139
 4140        return await _context.WorldMembers
 4141            .AnyAsync(wm => wm.WorldId == campaign.WorldId && wm.UserId == userId);
 4142    }
 143
 144    public async Task<bool> UserIsGMAsync(Guid campaignId, Guid userId)
 145    {
 146        // User is GM if they have GM role in the campaign's world
 9147        var campaign = await _context.Campaigns.FindAsync(campaignId);
 9148        if (campaign == null)
 0149            return false;
 150
 9151        return await _context.WorldMembers
 9152            .AnyAsync(wm => wm.WorldId == campaign.WorldId
 9153                        && wm.UserId == userId
 9154                        && wm.Role == WorldRole.GM);
 9155    }
 156
 157    public async Task<bool> ActivateCampaignAsync(Guid campaignId, Guid userId)
 158    {
 6159        var campaign = await _context.Campaigns
 6160            .FirstOrDefaultAsync(c => c.Id == campaignId);
 161
 6162        if (campaign == null)
 1163            return false;
 164
 165        // Only GM can activate
 5166        if (!await UserIsGMAsync(campaignId, userId))
 1167            return false;
 168
 169        // Deactivate all campaigns in the same world
 4170        var worldCampaigns = await _context.Campaigns
 4171            .Where(c => c.WorldId == campaign.WorldId && c.IsActive)
 4172            .ToListAsync();
 173
 10174        foreach (var c in worldCampaigns)
 175        {
 1176            c.IsActive = false;
 177        }
 178
 179        // Activate this campaign
 4180        campaign.IsActive = true;
 181
 4182        await _context.SaveChangesAsync();
 183
 4184        _logger.LogDebug("Activated campaign {CampaignId} in world {WorldId}", campaignId, campaign.WorldId);
 185
 4186        return true;
 6187    }
 188
 189    public async Task<ActiveContextDto> GetActiveContextAsync(Guid worldId, Guid userId)
 190    {
 4191        var result = new ActiveContextDto();
 4192        result.WorldId = worldId;
 193
 194        // Check if user has access to this world
 4195        var hasAccess = await _context.WorldMembers
 4196            .AnyAsync(wm => wm.WorldId == worldId && wm.UserId == userId);
 197
 4198        if (!hasAccess)
 1199            return result;
 200
 201        // Get all campaigns in this world
 3202        var campaigns = await _context.Campaigns
 3203            .Where(c => c.WorldId == worldId)
 3204            .ToListAsync();
 205
 3206        if (campaigns.Count == 0)
 0207            return result;
 208
 209        // Find explicitly active campaign only
 6210        Campaign? activeCampaign = campaigns.FirstOrDefault(c => c.IsActive);
 211
 3212        if (activeCampaign == null)
 1213            return result;
 214
 2215        result.CampaignId = activeCampaign.Id;
 2216        result.CampaignName = activeCampaign.Name;
 217
 218        // Get arcs for the active campaign
 2219        var arcs = await _context.Arcs
 2220            .Where(a => a.CampaignId == activeCampaign.Id)
 2221            .OrderBy(a => a.SortOrder)
 2222            .ToListAsync();
 223
 2224        if (arcs.Count == 0)
 0225            return result;
 226
 227        // Find explicitly active arc only
 4228        Arc? activeArc = arcs.FirstOrDefault(a => a.IsActive);
 229
 2230        if (activeArc != null)
 231        {
 1232            result.ArcId = activeArc.Id;
 1233            result.ArcName = activeArc.Name;
 234        }
 235
 2236        return result;
 4237    }
 238
 239    private static CampaignDto MapToDto(Campaign campaign)
 240    {
 3241        return new CampaignDto
 3242        {
 3243            Id = campaign.Id,
 3244            WorldId = campaign.WorldId,
 3245            Name = campaign.Name,
 3246            Description = campaign.Description,
 3247            OwnerId = campaign.OwnerId,
 3248            OwnerName = campaign.Owner?.DisplayName ?? "Unknown",
 3249            CreatedAt = campaign.CreatedAt,
 3250            StartedAt = campaign.StartedAt,
 3251            EndedAt = campaign.EndedAt,
 3252            IsActive = campaign.IsActive,
 3253            ArcCount = campaign.Arcs?.Count ?? 0
 3254        };
 255    }
 256
 257    private static CampaignDetailDto MapToDetailDto(Campaign campaign)
 258    {
 1259        return new CampaignDetailDto
 1260        {
 1261            Id = campaign.Id,
 1262            WorldId = campaign.WorldId,
 1263            Name = campaign.Name,
 1264            Description = campaign.Description,
 1265            OwnerId = campaign.OwnerId,
 1266            OwnerName = campaign.Owner?.DisplayName ?? "Unknown",
 1267            CreatedAt = campaign.CreatedAt,
 1268            StartedAt = campaign.StartedAt,
 1269            EndedAt = campaign.EndedAt,
 1270            IsActive = campaign.IsActive,
 1271            ArcCount = campaign.Arcs?.Count ?? 0,
 1272            Arcs = campaign.Arcs?.Select(a => new ArcDto
 1273            {
 1274                Id = a.Id,
 1275                CampaignId = a.CampaignId,
 1276                Name = a.Name,
 1277                Description = a.Description,
 1278                SortOrder = a.SortOrder,
 1279                IsActive = a.IsActive,
 1280                CreatedAt = a.CreatedAt
 1281            }).ToList() ?? new List<ArcDto>()
 1282        };
 283    }
 284}