using PostFundManagement.Application.Services.Shared; using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Application.Services.Grants; public sealed class GrantsService(IDbContextFactory contextFactory, HashService hashService) : IService { public async ValueTask> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var contracts = await context.Contracts.AsNoTracking() .OrderByDescending(c => c.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(contracts.Select(c => c.Map()).ToArray()); })); public async ValueTask> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(contractIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var contract = await context.Contracts.AsNoTracking() .FirstOrDefaultAsync(c => c.Id == decodeResult.Value, cancellationToken); return contract is not null ? Result.Ok(contract.Map()) : Result.Fail(NotFoundError.New); })); public async ValueTask> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var budgets = await context.Budgets.AsNoTracking() .OrderByDescending(b => b.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(budgets.Select(b => b.Map()).ToArray()); })); public async ValueTask> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(budgetIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var budget = await context.Budgets.AsNoTracking() .FirstOrDefaultAsync(b => b.Id == decodeResult.Value, cancellationToken); return budget is not null ? Result.Ok(budget.Map()) : Result.Fail(NotFoundError.New); })); public async ValueTask> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var awards = await context.Awards.AsNoTracking() .OrderByDescending(a => a.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(awards.Select(a => a.Map()).ToArray()); })); public async ValueTask> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(awardIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var award = await context.Awards.AsNoTracking().FirstOrDefaultAsync(a => a.Id == decodeResult.Value, cancellationToken); return award is not null ? Result.Ok(award.Map()) : Result.Fail(NotFoundError.New); })); public async ValueTask ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var changedTotal = await context.Contracts .Where(c => c.Id == request.ContractId && c.Status != ContractStatus.Active) .ExecuteUpdateAsync(f => f .SetProperty(u => u.ExternalSignatureId, request.ExternalSignatureId) .SetProperty(u => u.SignedDocumentUrl, request.SignedDocumentUrl) .SetProperty(u => u.CreatedBy, userId) .SetProperty(u => u.Status, ContractStatus.Active), cancellationToken); return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, UpdateError.New); })); public async ValueTask CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var award = await context.Awards.AsNoTracking() .FirstOrDefaultAsync(a => a.Id == request.AwardId, cancellationToken); if (award == null) return Result.Fail(NotFoundError.New); var contract = new Domain.Entities.Contract { AwardId = request.AwardId, TotalValue = request.TotalValue, EffectiveAt = request.EffectiveAt, ExpiresAt = request.ExpiresAt, Status = ContractStatus.Draft, CreatedBy = userId, CreatedAt = DateTime.UtcNow }; context.Contracts.Add(contract); return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New); })); public async ValueTask CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { if (request.Amount <= 0) return Result.Fail(ValidationError.Create("Budget amount must be greater than zero.")); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) return Result.Fail(NotFoundError.New); var budget = new Domain.Entities.Budget { ProjectId = request.ProjectId, Name = request.Name, Amount = request.Amount, CreatedBy = userId, CreatedAt = DateTime.UtcNow }; context.Budgets.Add(budget); return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New); })); public async ValueTask CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { if (request.Amount <= 0) return Result.Fail(ValidationError.Create("Approved amount must be greater than zero.")); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Organisations.AnyAsync(o => o.Id == request.OrganisationId, cancellationToken)) return Result.Fail(NotFoundError.New); if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken)) return Result.Fail(NotFoundError.New); var award = new Domain.Entities.Award { OrganisationId = request.OrganisationId, ProgrammeId = request.ProgrammeId, Amount = request.Amount, Status = ApprovalStatus.Pending, CreatedBy = userId, CreatedAt = DateTime.UtcNow }; context.Awards.Add(award); return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New); })); }