using PostFundManagement.Application.Shared; using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; 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) { try { 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() ?? []); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetContractByIdAsync(string contractIdHash, CancellationToken cancellationToken = default) { try { var decodeResult = hashService.DecodeLongIdHash(contractIdHash); if (decodeResult.IsFailed) return Result.Fail(decodeResult.Errors); 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("Failed to find specific contract"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) { try { 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() ?? []); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetBudgetByIdAsync(string budgetIdHash, CancellationToken cancellationToken = default) { try { var decodeResult = hashService.DecodeLongIdHash(budgetIdHash); if (decodeResult.IsFailed) return Result.Fail(decodeResult.Errors); 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("Failed to get specific budget."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) { try { 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() ?? []); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetAwardByIdAsync(string awardIdHash, CancellationToken cancellationToken = default) { try { var decodeResult = hashService.DecodeLongIdHash(awardIdHash); if (decodeResult.IsFailed) return Result.Fail(decodeResult.Errors); 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("Award not found"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default) { try { 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 changedTotal > 0 ? Result.Ok() : Result.Fail("Contract activation failed"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) { try { 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($"Award with ID {request.AwardId} does not exist."); 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 await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail("Failed to create contract"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) { try { if (request.Amount <= 0) return Result.Fail("Budget amount must be greater than zero."); using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) return Result.Fail($"Project with ID {request.ProjectId} does not exist."); 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 await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail("Failed to create budget"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) { try { if (request.Amount <= 0) return Result.Fail("Approved amount must be greater than zero."); using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Organisations.AnyAsync(o => o.Id == request.OrganisationId, cancellationToken)) return Result.Fail($"Organisation with ID {request.OrganisationId} does not exist."); if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken)) return Result.Fail($"Programme with ID {request.ProgrammeId} does not exist."); 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 await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail("Failed to create award."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } }