using PostFundManagement.Application.Abstractions.Services; 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.Communications; public sealed class CommunicationsService(IDbContextFactory contextFactory, HashService hashService, ITenantContextService tenantContext) : ICommunicationsService { public async Task> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var recipient = await context.TenantUsers.AsNoTracking() .Include(i => i.User) .Include(i => i.Tenant) .FirstOrDefaultAsync(u => u.UserId == request.Recipient && u.Status == ActivityStatus.Active && u.Tenant!.Status == ActivityStatus.Active); if (recipient is null) return Result.Fail(ValidationError.Create("Tenant or User is not active, invite not sent")); var invite = new Domain.Entities.Invite { OrganisationId = request.OrganisationId, InvitedOrganisationId = request.InvitedOrganisationId, AwardId = request.AwardId, ContractId = request.ContractId, Recipient = recipient.UserId, CreatedBy = userId, CreatedAt = DateTime.UtcNow, TenantId = tenantContext.TenantId }; context.Invites.Add(invite); var notificationRequest = new CreateNotificationRequest(request.InvitedOrganisationId, recipient.UserId, request.Platform, "Funding award invite", recipient!.User!.Email, request.Message); var notificationResult = await CreateNotificationAsync(notificationRequest, userId, cancellationToken); return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0 && notificationResult.IsFailed, CreateError.Create("Failed to create invite, contact support").CausedBy(notificationResult.Errors)) .ToResult(invite?.Map()!); })); public async Task> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var invites = await context.Invites.AsNoTracking() .OrderByDescending(i => i.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(invites.Select(i => i.Map()).ToArray()); })); public async Task> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var invite = await context.Invites.AsNoTracking() .OrderByDescending(o => o.Id) .FirstOrDefaultAsync(i => i.Recipient == applicantUserId, cancellationToken); return Result.FailIf(invite == null, NotFoundError.Create("No active invite found")) .ToResult(invite?.Map()!); })); public async Task> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var invites = await context.Invites.AsNoTracking() .Where(i => i.Recipient == applicantUserId) .OrderByDescending(o => o.Id) .ToListAsync(cancellationToken); return Result.Ok(invites?.Select(i => i.Map()).ToArray() ?? []); })); public async Task CreateNotificationAsync(CreateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var initialStatus = request.Status ?? (request.Platform == NotificationPlatform.InApp ? NotificationStatus.Delivered : NotificationStatus.Queued); context.Notifications.Add(new Domain.Entities.Notification { CreatedAt = DateTime.UtcNow, Recipient = request.Recipient, OrganisationId = request.OrganisationId, Destination = request.Destination, Subject = request.Subject, Message = request.Message, Platform = request.Platform, Status = initialStatus, TenantId = tenantContext.TenantId }); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create entry, contact support")); })); public async Task UpdateNotificationAsync(UpdateNotificationRequest request, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var entity = await context.Notifications.FirstOrDefaultAsync(n => n.Id == request.NotificationId, cancellationToken); if (entity is null) return Result.Fail(NotFoundError.Create($"Notification #{request.NotificationId} not found")); entity.Status = request.Status; entity.UpdatedAt = DateTime.UtcNow; if (request.SentAt.HasValue) entity.SentAt = request.SentAt.Value; if (!string.IsNullOrWhiteSpace(request.Destination)) entity.Destination = request.Destination; if (!string.IsNullOrWhiteSpace(request.Error)) entity.Error = request.Error; return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, UpdateError.Create($"Failed to update notification: {request.NotificationId}, contact support")); })); public async Task> GetNotificationsAsync(string organisationIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(organisationIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var notifications = await context.Notifications.AsNoTracking() .Where(n => n.OrganisationId == decodeResult.Value) .OrderByDescending(o => o.Id) .ToListAsync(cancellationToken); return Result.Ok(notifications?.Select(i => i.Map()).ToArray() ?? []); })); public async Task> GetUserNotificationsAsync(Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var notifications = await context.Notifications.AsNoTracking() .Where(n => n.Recipient == userId) .OrderByDescending(o => o.CreatedAt) .ToListAsync(cancellationToken); return Result.Ok(notifications?.Select(i => i.Map()).ToArray() ?? []); })); public async Task> GetNotificationAsync(string notificationIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(notificationIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var notification = await context.Notifications.AsNoTracking() .FirstOrDefaultAsync(n => n.Id == decodeResult.Value, cancellationToken); return Result.FailIf(notification is null, NotFoundError.Create("Notification not found")) .ToResult(notification?.Map()!); })); public async Task> GetApplicantDataAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var invites = await context.Invites.AsNoTracking() .Where(i => i.Recipient == applicantUserId) .OrderByDescending(o => o.Id) .ToListAsync(cancellationToken); var applicantData = new List(); foreach (var invite in invites) { var outputInvite = invite.Map(); var contract = (await context.Contracts.AsNoTracking() .FirstOrDefaultAsync(c => c.Id == invite.ContractId, cancellationToken))?.Map(); var fundingOrganisation = (await context.Organisations.AsNoTracking() .FirstOrDefaultAsync(o => o.Id == invite.OrganisationId, cancellationToken))?.Map(); var invitedOrganisation = (await context.Organisations.AsNoTracking() .FirstOrDefaultAsync(o => o.Id == invite.InvitedOrganisationId, cancellationToken))?.Map(); var awards = (await context.Awards.AsNoTracking() .Where(a => a.Id == invite.AwardId) .ToListAsync(cancellationToken))?.Select(a => a.Map()).ToArray(); var projectIds = (await context.Awards.AsNoTracking() .Where(a => a.Id == invite.AwardId && a.ProjectId.HasValue) .Select(a => a.ProjectId!.Value) .Distinct() .ToListAsync(cancellationToken)) ?? []; var projects = (await context.Projects.AsNoTracking() .Where(p => projectIds.Contains(p.Id)) .ToListAsync(cancellationToken))?.Select(p => p.Map()).ToArray() ?? []; var data = new ApplicantData { Invite = outputInvite, Contract = contract, Awards = awards!, FundingOrganisation = fundingOrganisation, InvitedOrganisation = invitedOrganisation, Projects = projects }; applicantData.Add(data); } return Result.Ok(applicantData.ToArray()); })); }