using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Services.Shared; using PostFundManagement.Domain; using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Application.Services.Projects; public sealed class ProjectService(IDbContextFactory contextFactory, HashService hashService) : IProjectService { public async ValueTask UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var project = await context.Projects.FirstOrDefaultAsync(p => p.Id == request.ProjectId, cancellationToken); if (project == null) return Result.Fail(NotFoundError.New); project.Status = request.Status; project.UpdatedBy = userId; project.UpdatedAt = DateTime.UtcNow; context.Projects.Update(project); var changedTotal = await context.Projects .Where(p => p.Id != request.ProjectId) .ExecuteUpdateAsync(f => f .SetProperty(p => p.Status, request.Status) .SetProperty(p => p.UpdatedBy, userId) .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); return Result.FailIf(changedTotal == 0, UpdateError.New); })); public async ValueTask> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var projects = await context.Projects.AsNoTracking().OrderByDescending(p => p.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(projects.Select(p => p.Map()).ToArray()); })); public async ValueTask> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(projectIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var project = await context.Projects.AsNoTracking() .FirstOrDefaultAsync(p => p.Id == decodeResult.Value, cancellationToken); return project is not null ? Result.Ok(project.Map()) : Result.Fail(NotFoundError.Create()); })); public async ValueTask> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(projectIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create().CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var activities = await context.Activities.AsNoTracking() .Where(a => a.ProjectId == decodeResult.Value) .OrderByDescending(a => a.CreatedAt) .ToListAsync(cancellationToken); return Result.Ok(activities.Select(a => a.Map()).ToArray()); })); public async ValueTask CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (await context.Projects.AnyAsync(p => p.Name == request.Name && p.Sector == request.Sector && p.ProjectType == request.ProjectType, cancellationToken)) return Result.Fail(ValidationError.Create("A similarly named project exists of the same project type and sector")); if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken)) return Result.Fail(NotFoundError.New); context.Projects.Add(new Domain.Entities.Project { ProgrammeId = request.ProgrammeId, Name = request.Name, Description = request.Description, Sector = request.Sector, ProjectType = request.ProjectType, Status = ApprovalStatus.Pending, CreatedBy = userId, CreatedAt = DateTime.UtcNow }); return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New); })); public async ValueTask CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) return Result.Fail(NotFoundError.New); if (request.MilestoneId.HasValue) if (!await context.Milestones.AnyAsync(m => m.Id == request.MilestoneId.Value, cancellationToken)) return Result.Fail(NotFoundError.New); context.Activities.Add(new Domain.Entities.Activity { ProjectId = request.ProjectId, MilestoneId = request.MilestoneId, Category = request.Category, Amount = request.Amount, Title = request.Title, Description = request.Description, PerformedAt = request.PerformedAt, Status = ApprovalStatus.Pending, CreatedBy = userId, CreatedAt = DateTime.UtcNow }); return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New); })); public async ValueTask> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(projectIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var milestones = await context.Milestones.AsNoTracking() .Where(m => m.ProjectId == decodeResult.Value) .OrderBy(m => m.DueAt) .ToListAsync(cancellationToken); return Result.Ok(milestones.Select(i => i.Map()).ToArray()); })); public async ValueTask> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(projectIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var evidences = await context.Indicators.AsNoTracking() .Where(e => e.ProjectId == decodeResult.Value) .OrderByDescending(o => o.Id) .ToListAsync(cancellationToken); return Result.Ok(evidences.Select(e => e.Map()).ToArray()); })); public async ValueTask> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create().CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var milestone = await context.Milestones.AsNoTracking() .FirstOrDefaultAsync(m => m.Id == decodeResult.Value, cancellationToken); return milestone is not null ? Result.Ok(milestone.Map()) : Result.Fail(NotFoundError.Create()); })); public async ValueTask> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var milestones = await context.Milestones.AsNoTracking() .OrderByDescending(m => m.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(milestones.Select(i => i.Map()).ToArray()); })); public async ValueTask UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var changedTotal = await context.Milestones.Where(m => m.Id == request.MilestoneId) .ExecuteUpdateAsync(f => f .SetProperty(u => u.Name, request.Name) .SetProperty(u => u.DueAt, request.DueAt) .SetProperty(u => u.Priority, request.Priority) .SetProperty(u => u.Status, request.Status) .SetProperty(u => u.UpdatedBy, userId) .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); return Result.FailIf(changedTotal <= 0, UpdateError.New); })); public async ValueTask UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var changedTotal = await context.Projects.Where(p => p.Id == request.ProjectId) .ExecuteUpdateAsync(f => f .SetProperty(u => u.Name, request.Name) .SetProperty(u => u.Description, request.Description) .SetProperty(u => u.StartedAt, request.StartedAt) .SetProperty(u => u.EndedAt, request.EndedAt) .SetProperty(u => u.UpdatedBy, userId) .SetProperty(u => u.UpdatedAt, DateTime.UtcNow) .SetProperty(u => u.Sector, request.Sector) .SetProperty(u => u.ProjectType, request.ProjectType), cancellationToken); return Result.FailIf(changedTotal <= 0, UpdateError.New); })); public async ValueTask> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(activityIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var activity = await context.Activities.AsNoTracking() .FirstOrDefaultAsync(a => a.Id == decodeResult.Value, cancellationToken); return activity is not null ? Result.Ok(activity.Map()) : Result.Fail(NotFoundError.New); })); public async ValueTask> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(projectIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var risks = await context.Risks.AsNoTracking() .Where(r => r.ProjectId == decodeResult.Value) .OrderByDescending(r => r.CreatedAt) .ToListAsync(cancellationToken); return Result.Ok(risks.Select(i => i.Map()).ToArray()); })); public async ValueTask CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken)) return Result.Fail(NotFoundError.New); if (await context.Programmes.AnyAsync(p => p.Name == request.Name, cancellationToken)) return Result.Fail(ValidationError.Create("Programme of the same name exists")); context.Programmes.Add(new Domain.Entities.Programme { OwnedBy = request.OwnerId, CreatedBy = userId, CreatedAt = DateTime.UtcNow, Status = ApprovalStatus.Pending }); return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New); })); public async ValueTask UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken)) return Result.Fail(NotFoundError.New); var changedTotal = await context.Programmes.Where(p => p.Id == request.ProgrammeId) .ExecuteUpdateAsync(f => f .SetProperty(u => u.Name, request.Name) .SetProperty(u => u.OwnedBy, request.OwnerId) .SetProperty(u => u.Status, request.Status) .SetProperty(u => u.UpdatedBy, userId) .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); return Result.FailIf(changedTotal <= 0, UpdateError.New); })); public async ValueTask> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(programmeIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var programme = await context.Programmes.FirstOrDefaultAsync(p => p.Id == decodeResult.Value, cancellationToken); return programme is not null ? Result.Ok(programme.Map()) : Result.Fail(NotFoundError.New); })); public async ValueTask> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var programmes = await context.Programmes.AsNoTracking() .OrderByDescending(m => m.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(programmes.Select(i => i.Map()).ToArray()); })); public async ValueTask> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(programmeIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Programmes.AnyAsync(p => p.Id == decodeResult.Value, cancellationToken)) return Result.Fail(NotFoundError.New); var projects = await context.Projects.AsNoTracking() .Where(p => p.ProgrammeId == decodeResult.Value) .OrderByDescending(o => o.CreatedAt) .ToListAsync(cancellationToken); return Result.Ok(projects.Select(i => i.Map()).ToArray()); })); }