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 is 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); 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 = NotificationStatus.Queued, 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 changedTotal = await context.Notifications .Where(n => n.Id == request.NotificationId) .ExecuteUpdateAsync(f => f .SetProperty(u => u.Destination, request.Destination) .SetProperty(u => u.SentAt, request.SentAt) .SetProperty(u => u.Error, request.Error) .SetProperty(u => u.UpdatedAt, DateTime.UtcNow) .SetProperty(u => u.Status, request.Status), cancellationToken); return Result.OkIf(changedTotal > 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> 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()); })); }