diff --git a/PostFundManagement.Api/Endpoints/Communications/GetInvitesEndpoint.cs b/PostFundManagement.Api/Endpoints/Communications/GetInvitesEndpoint.cs index b4387d8..1069343 100644 --- a/PostFundManagement.Api/Endpoints/Communications/GetInvitesEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Communications/GetInvitesEndpoint.cs @@ -1,7 +1,11 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; +using PostFundManagement.Application.Communications; +using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Api; using PostFundManagement.Domain.Extensions; -using PostFundManagement.Infrastructure.Database; +using PostFundManagement.Domain.Models; +using static PostFundManagement.Domain.Extensions.General; namespace PostFundManagement.Api.Endpoints.Communications; @@ -10,31 +14,20 @@ public class GetInvitesEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/communications/invites", async (IDbContextFactory contextFactory, - int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) => + builder.MapGet("api/communications/invites/{page:int}/{pageSize:int}", async (CommunicationsService service, int page = 1, int pageSize = 100, + CancellationToken cancellationToken = default) => { - if (page < 1) page = 1; - if (pageSize < 1) pageSize = 10; - if (pageSize > 100) pageSize = 100; + var result = await service.GetInvitesAsync(new Pagination { Page = page, PageSize = pageSize }, cancellationToken); - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var query = context.Invites.AsNoTracking(); - - var totalCount = await query.CountAsync(cancellationToken); - - var items = await query.OrderByDescending(i => i.CreatedAt) - .Skip((page - 1) * pageSize) - .Take(pageSize) - .ToListAsync(cancellationToken); - - return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize }); + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.BadRequest(result.Errors.ToProblems("Failed to get invites")); }) .RequireAuthorization() .WithDescription("Get a list of sent invitations") .WithName(typeof(GetInvitesEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) - .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .WithTags(EndpointTags.Communications); } diff --git a/PostFundManagement.Api/Endpoints/Communications/SendInviteEndpoint.cs b/PostFundManagement.Api/Endpoints/Communications/SendInviteEndpoint.cs index 2d9126f..9d0046d 100644 --- a/PostFundManagement.Api/Endpoints/Communications/SendInviteEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Communications/SendInviteEndpoint.cs @@ -1,55 +1,23 @@ -using PostFundManagement.Domain; +using PostFundManagement.Application.Communications; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Api; using PostFundManagement.Domain.Extensions; -using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Api.Endpoints.Communications; [ApiVersionTarget(1)] public class SendInviteEndpoint : IEndpoint { - public record InviteRequest(long OrganisationId, long InvitedOrganisationId, long AwardId, long ContractId, Guid Recipient, string Message, NotificationPlatform Platform); - public void Map(IEndpointRouteBuilder builder) { builder.MapPost("api/communications/invite", async (InviteRequest request, ClaimsPrincipal principal, - IDbContextFactory contextFactory, CancellationToken cancellationToken = default) => + CommunicationsService service, CancellationToken cancellationToken = default) => { - var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value; + var result = await service.SendInviteAsync(request, principal, cancellationToken); - if (!Guid.TryParse(sidClaim, out var currentUserId)) - return Results.BadRequest("Missing or invalid 'sid' claim."); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var invite = new Domain.Entities.Invite - { - OrganisationId = request.OrganisationId, - InvitedOrganisationId = request.InvitedOrganisationId, - AwardId = request.AwardId, - ContractId = request.ContractId, - Recipient = request.Recipient, - CreatedBy = currentUserId, - CreatedAt = DateTime.UtcNow - }; - - context.Invites.Add(invite); - - context.Notifications.Add(new Domain.Entities.Notification - { - OrganisationId = request.OrganisationId, - Recipient = request.Recipient, - Platform = request.Platform, - Status = NotificationStatus.Pending, - Subject = "Invitation to Onboard", - Message = request.Message, - CreatedAt = DateTime.UtcNow - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Results.Ok(invite) - : Results.BadRequest("Failed to create invite"); + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.BadRequest(result.Errors.ToProblems("Failed to create invite")); }) .RequireAuthorization() .WithDescription("Trigger an invite to an organization or candidate (specifying NotificationPlatform)") diff --git a/PostFundManagement.Api/Endpoints/Communications/SyncEndpoint.cs b/PostFundManagement.Api/Endpoints/Communications/SyncUserEndpoint.cs similarity index 56% rename from PostFundManagement.Api/Endpoints/Communications/SyncEndpoint.cs rename to PostFundManagement.Api/Endpoints/Communications/SyncUserEndpoint.cs index acce462..844f914 100644 --- a/PostFundManagement.Api/Endpoints/Communications/SyncEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Communications/SyncUserEndpoint.cs @@ -1,41 +1,27 @@ using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Api; +using PostFundManagement.Domain.Events.Communications; using PostFundManagement.Domain.Extensions; namespace PostFundManagement.Api.Endpoints.Communications; -public class SyncEvent : EventBase, IEvent -{ - public string Name { get; set; } = nameof(SyncEvent); -} - -public class SyncEventHandler : INotificationHandler -{ - public ValueTask Handle(SyncEvent notification, CancellationToken cancellationToken) - { - Trace.WriteLine($"Integration Sync executed. Correlation ID: {notification.CorrelationId}"); - - return ValueTask.CompletedTask; - } -} - [ApiVersionTarget(1)] -public class SyncEndpoint : IEndpoint +public class SyncUserEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { builder.MapPost("api/integration/sync", async (IJobOrchestrator jobOrchestrator, CancellationToken cancellationToken = default) => { - var syncEvent = new SyncEvent(); + var syncEvent = new SyncUserEvent(); await jobOrchestrator.SendAsync(syncEvent, cancellationToken); - return Results.Ok(new { Queued = true, syncEvent.CorrelationId }); + return Results.Ok(); }) .RequireAuthorization() .WithDescription("Trigger integration and data synchronization in the background") - .WithName(typeof(SyncEndpoint).ToEndpointName()) + .WithName(typeof(SyncUserEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) diff --git a/PostFundManagement.Api/Endpoints/Evidence/CreateIndicatorEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/CreateIndicatorEndpoint.cs index 7051e83..3bab538 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/CreateIndicatorEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Evidence/CreateIndicatorEndpoint.cs @@ -1,54 +1,30 @@ -using PostFundManagement.Domain; +using PostFundManagement.Application.Evidence; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Api; using PostFundManagement.Domain.Extensions; -using PostFundManagement.Infrastructure.Database; +using PostFundManagement.Domain.Models; namespace PostFundManagement.Api.Endpoints.Evidence; [ApiVersionTarget(1)] public class CreateIndicatorEndpoint : IEndpoint { - public record CreateIndicatorRequest(long ProjectId, string Name, UnitOfMeasure UnitOfMeasure, decimal BaselineAmount, decimal TargetAmount); - public void Map(IEndpointRouteBuilder builder) { builder.MapPost("api/evidence/indicators", async (CreateIndicatorRequest request, ClaimsPrincipal principal, - IDbContextFactory contextFactory, CancellationToken cancellationToken = default) => + EvidenceService service, CancellationToken cancellationToken = default) => { - var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value; + var result = await service.CreateIndicatorAsync(request, principal, cancellationToken); - if (!Guid.TryParse(sidClaim, out var userId)) - return Results.BadRequest("Missing or invalid 'sid' claim."); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) - return Results.BadRequest($"Project with ID {request.ProjectId} does not exist."); - - var indicator = new Domain.Entities.Indicator - { - ProjectId = request.ProjectId, - Name = request.Name, - UnitOfMeasure = request.UnitOfMeasure, - BaselineAmount = request.BaselineAmount, - TargetAmount = request.TargetAmount, - ActualAmount = request.BaselineAmount, - CreatedBy = userId, - CreatedAt = DateTime.UtcNow - }; - - context.Indicators.Add(indicator); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Results.Created($"api/evidence/indicators/{indicator.Id}", indicator) - : Results.BadRequest("Failed to create indicator"); + return result.IsSuccess + ? Results.Created($"api/evidence/indicators/{result.Value.Id}", result.Value.Indicator) + : Results.BadRequest(result.Errors.ToProblems("Failed to create indicator")); }) .RequireAuthorization() .WithDescription("Define indicators and baselines/targets (M08)") .WithName(typeof(CreateIndicatorEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) - .Produces(StatusCodes.Status201Created) + .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) .WithTags(EndpointTags.Evidence); diff --git a/PostFundManagement.Api/Endpoints/Evidence/CreateMilestoneEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/CreateMilestoneEndpoint.cs index 133fb0d..25e3096 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/CreateMilestoneEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Evidence/CreateMilestoneEndpoint.cs @@ -1,53 +1,30 @@ -using PostFundManagement.Domain; +using PostFundManagement.Application.Evidence; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Api; using PostFundManagement.Domain.Extensions; -using PostFundManagement.Infrastructure.Database; +using PostFundManagement.Domain.Models; namespace PostFundManagement.Api.Endpoints.Evidence; [ApiVersionTarget(1)] public class CreateMilestoneEndpoint : IEndpoint { - public record CreateMilestoneRequest(long ProjectId, DateTime DueAt, string Name, Priority Priority); - public void Map(IEndpointRouteBuilder builder) { builder.MapPost("api/evidence/milestones", async (CreateMilestoneRequest request, ClaimsPrincipal principal, - IDbContextFactory contextFactory, CancellationToken cancellationToken = default) => + EvidenceService service, CancellationToken cancellationToken = default) => { - var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value; + var result = await service.CreateMilestoneAsync(request, principal, cancellationToken); - if (!Guid.TryParse(sidClaim, out var userId)) - return Results.BadRequest("Missing or invalid 'sid' claim."); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) - return Results.BadRequest($"Project with ID {request.ProjectId} does not exist."); - - var milestone = new Domain.Entities.Milestone - { - ProjectId = request.ProjectId, - DueAt = request.DueAt, - Name = request.Name, - Priority = request.Priority, - Status = ApprovalStatus.Pending, - CreatedBy = userId, - CreatedAt = DateTime.UtcNow - }; - - context.Milestones.Add(milestone); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Results.Created($"api/evidence/milestones/{milestone.Id}", milestone) - : Results.BadRequest("Failed to create a new milestone"); + return result.IsSuccess + ? Results.Created($"api/evidence/milestones/{result.Value.Id}", result.Value.Milestone) + : Results.BadRequest(result.Errors.ToProblems("Failed to create a new milestone")); }) .RequireAuthorization() .WithDescription("Define contractual milestones (M07)") .WithName(typeof(CreateMilestoneRequest).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) - .Produces(StatusCodes.Status201Created) + .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) .WithTags(EndpointTags.Evidence); diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetEvidenceFilesEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/GetEvidenceFilesEndpoint.cs index 280c62d..aa11971 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/GetEvidenceFilesEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Evidence/GetEvidenceFilesEndpoint.cs @@ -1,7 +1,7 @@ +using PostFundManagement.Application.Evidence; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Api; using PostFundManagement.Domain.Extensions; -using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Api.Endpoints.Evidence; @@ -10,31 +10,19 @@ public class GetEvidenceFilesEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/evidence/files", async (IDbContextFactory contextFactory, - int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) => + builder.MapGet("api/evidence/files/{page:int}/{pageSize:int}", async (EvidenceService service, int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => { - if (page < 1) page = 1; - if (pageSize < 1) pageSize = 10; - if (pageSize > 100) pageSize = 100; + var result = await service.GetEvidenceFilesAsync(new Domain.Pagination(page, pageSize), cancellationToken); - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var query = context.Evidences.AsNoTracking(); - - var totalCount = await query.CountAsync(cancellationToken); - - var items = await query.OrderByDescending(e => e.CreatedAt) - .Skip((page - 1) * pageSize) - .Take(pageSize) - .ToListAsync(cancellationToken); - - return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize }); + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.BadRequest(result.Errors.ToProblems("Failed to get evidence")); }) .RequireAuthorization() .WithDescription("Get a list of all uploaded evidence files metadata") .WithName(typeof(GetEvidenceFilesEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) - .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .WithTags(EndpointTags.Evidence); } diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetIndicatorsEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/GetIndicatorsEndpoint.cs index 1fb98b4..a9828dc 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/GetIndicatorsEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Evidence/GetIndicatorsEndpoint.cs @@ -1,42 +1,29 @@ +using PostFundManagement.Application.Evidence; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Api; using PostFundManagement.Domain.Extensions; -using PostFundManagement.Infrastructure.Database; +using PostFundManagement.Domain.Models; namespace PostFundManagement.Api.Endpoints.Evidence; [ApiVersionTarget(1)] public class GetIndicatorsEndpoint : IEndpoint { - public record RecordActualRequest(decimal ActualAmount); - public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/evidence/indicators", async (IDbContextFactory contextFactory, - int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) => + builder.MapGet("api/evidence/indicators/{page:int}/{pageSize:int}", async (EvidenceService service, int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => { - if (page < 1) page = 1; - if (pageSize < 1) pageSize = 10; - if (pageSize > 100) pageSize = 100; + var result = await service.GetIndicatorsAsync(new Domain.Pagination { Page = page, PageSize = pageSize }, cancellationToken); - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var query = context.Indicators.AsNoTracking(); - - var totalCount = await query.CountAsync(cancellationToken); - - var items = await query.OrderByDescending(i => i.CreatedAt) - .Skip((page - 1) * pageSize) - .Take(pageSize) - .ToListAsync(cancellationToken); - - return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize }); + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.BadRequest(result.Errors.ToProblems("Failed to get indicators")); }) .RequireAuthorization() .WithDescription("Get a list of indicators") .WithName(typeof(GetIndicatorsEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) - .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .WithTags(EndpointTags.Evidence); } diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetMilestonesEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/GetMilestonesEndpoint.cs index 3594afe..19531e9 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/GetMilestonesEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Evidence/GetMilestonesEndpoint.cs @@ -1,7 +1,8 @@ +using PostFundManagement.Application.Evidence; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Api; +using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Extensions; -using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Api.Endpoints.Evidence; @@ -10,31 +11,20 @@ public class GetMilestonesEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/evidence/milestones", async (IDbContextFactory contextFactory, + builder.MapGet("api/evidence/milestones/{page:int}/{pageSize:int}", async (EvidenceService service, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) => { - if (page < 1) page = 1; - if (pageSize < 1) pageSize = 10; - if (pageSize > 100) pageSize = 100; + var result = await service.GetMilestonesAsync(new Domain.Pagination { Page = page, PageSize = pageSize }, cancellationToken); - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var query = context.Milestones.AsNoTracking(); - - var totalCount = await query.CountAsync(cancellationToken); - - var items = await query.OrderByDescending(m => m.CreatedAt) - .Skip((page - 1) * pageSize) - .Take(pageSize) - .ToListAsync(cancellationToken); - - return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize }); + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.BadRequest(result.Errors.ToProblems("Failed to fetch milestones")); }) .RequireAuthorization() .WithDescription("Get a list of all milestones") .WithName(typeof(GetMilestonesEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) - .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .WithTags(EndpointTags.Evidence); } diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetProjectMilestonesEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/GetProjectMilestonesEndpoint.cs index 279f60e..55d7bae 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/GetProjectMilestonesEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Evidence/GetProjectMilestonesEndpoint.cs @@ -1,7 +1,8 @@ +using PostFundManagement.Application.Evidence; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Api; +using PostFundManagement.Domain.Configuration.Entities; using PostFundManagement.Domain.Extensions; -using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Api.Endpoints.Evidence; @@ -11,23 +12,21 @@ public class GetProjectMilestonesEndpoint : IEndpoint public void Map(IEndpointRouteBuilder builder) { builder.MapGet("api/evidence/milestones/project/{projectId:long}", async (long projectId, - IDbContextFactory contextFactory, CancellationToken cancellationToken = default) => + EvidenceService service, CancellationToken cancellationToken = default) => { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + var result = await service.GetMilestonesByProjectIdAsync(projectId, cancellationToken); - var milestones = await context.Milestones.AsNoTracking() - .Where(m => m.ProjectId == projectId) - .OrderBy(m => m.DueAt) - .ToListAsync(cancellationToken); - - return Results.Ok(milestones); + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.NotFound(); }) .RequireAuthorization() .WithDescription("Get a list of milestones for a project") .WithName(typeof(GetProjectMilestonesEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) - .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) .WithTags(EndpointTags.Evidence); } } diff --git a/PostFundManagement.Api/Endpoints/Evidence/RecordIndicatorActualAmountEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/RecordIndicatorActualAmountEndpoint.cs deleted file mode 100644 index 739b566..0000000 --- a/PostFundManagement.Api/Endpoints/Evidence/RecordIndicatorActualAmountEndpoint.cs +++ /dev/null @@ -1,40 +0,0 @@ -using PostFundManagement.Domain.Abstractions; -using PostFundManagement.Domain.Api; -using PostFundManagement.Domain.Extensions; -using PostFundManagement.Infrastructure.Database; - -namespace PostFundManagement.Api.Endpoints.Evidence; - -[ApiVersionTarget(1)] -public class RecordIndicatorActualAmountEndpoint : IEndpoint -{ - public void Map(IEndpointRouteBuilder builder) - { - builder.MapPost("api/evidence/indicators/{id:long}/actuals/{actualAmount:decimal}", async (long id, decimal actualAmount, - ClaimsPrincipal principal, IDbContextFactory contextFactory, CancellationToken cancellationToken = default) => - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var indicator = await context.Indicators.FirstOrDefaultAsync(i => i.Id == id, cancellationToken); - - if (indicator is null) - return Results.NotFound($"Indicator with ID {id} does not exist."); - - indicator.ActualAmount = actualAmount; - - context.Indicators.Update(indicator); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Results.Ok(indicator) - : Results.BadRequest("Failed to record actual amount"); - }) - .RequireAuthorization() - .WithDescription("Record actual values for indicators (M08)") - .WithName(typeof(RecordIndicatorActualAmountEndpoint).ToEndpointName()) - .MapToApiVersion(new ApiVersion(1)) - .Produces(StatusCodes.Status200OK) - .Produces(StatusCodes.Status404NotFound) - .Produces(StatusCodes.Status401Unauthorized) - .WithTags(EndpointTags.Evidence); - } -} diff --git a/PostFundManagement.Api/Endpoints/Evidence/RecordIndicatorAmountEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/RecordIndicatorAmountEndpoint.cs new file mode 100644 index 0000000..ac5d087 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Evidence/RecordIndicatorAmountEndpoint.cs @@ -0,0 +1,31 @@ +using PostFundManagement.Application.Evidence; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Api; +using PostFundManagement.Domain.Extensions; + +namespace PostFundManagement.Api.Endpoints.Evidence; + +[ApiVersionTarget(1)] +public class RecordIndicatorAmountEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPost("api/evidence/indicators/{id:string}/actuals/{actualAmount:decimal}", async (string id, decimal actualAmount, + ClaimsPrincipal principal, EvidenceService service, CancellationToken cancellationToken = default) => + { + var result = await service.RecordIndicatorActualAmountAsync(id, actualAmount, principal, cancellationToken); + + return result.IsSuccess + ? Results.Ok() + : Results.BadRequest("Failed to record actual amount"); + }) + .RequireAuthorization() + .WithDescription("Record actual values for indicators (M08)") + .WithName(typeof(RecordIndicatorAmountEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Evidence); + } +} diff --git a/PostFundManagement.Api/Endpoints/Evidence/UploadEvidenceFilesEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/UploadEvidenceFilesEndpoint.cs index 6ae0089..db8abad 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/UploadEvidenceFilesEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Evidence/UploadEvidenceFilesEndpoint.cs @@ -1,8 +1,7 @@ -using PostFundManagement.Domain; +using PostFundManagement.Application.Evidence; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Api; using PostFundManagement.Domain.Extensions; -using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Api.Endpoints.Evidence; @@ -11,8 +10,7 @@ public class UploadEvidenceFilesEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapPost("api/evidence/files", async (HttpRequest request, ClaimsPrincipal principal, IDbContextFactory contextFactory, - [FromKeyedServices(Constants.EvidenceS3SettingsSection)] IS3Service s3Service, CancellationToken cancellationToken = default) => + builder.MapPost("api/evidence/files", async (HttpRequest request, ClaimsPrincipal principal, EvidenceService service, CancellationToken cancellationToken = default) => { var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value; @@ -28,41 +26,10 @@ public class UploadEvidenceFilesEndpoint : IEndpoint if (file == null || file.Length == 0) return Results.BadRequest("No file uploaded or file is empty."); - _ = long.TryParse(form["projectId"], out var projectId); - _ = long.TryParse(form["milestoneId"], out var milestoneId); - _ = long.TryParse(form["indicatorId"], out var indicatorId); - _ = long.TryParse(form["activityId"], out var activityId); - _ = Enum.TryParse(form["type"], out var type); + var result = await service.UploadEvidenceFileAsync(userId, form, file, cancellationToken); - using var stream = file.OpenReadStream(); - - var uploadResult = await s3Service.UploadFileAsync(file.FileName, stream, file.ContentType, cancellationToken); - - if (uploadResult.IsFailed) - return Results.BadRequest(uploadResult.Errors.FirstOrDefault()?.Message ?? "File upload failed."); - - var documentUrl = uploadResult.Value; - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var evidence = new Domain.Entities.Evidence - { - ProjectId = projectId, - MilestoneId = milestoneId, - IndicatorId = indicatorId, - ActivityId = activityId, - CreatedBy = userId, - CreatedAt = DateTime.UtcNow, - Version = 1, - Type = type, - DocumentUrl = documentUrl, - Status = ApprovalStatus.Pending - }; - - context.Evidences.Add(evidence); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Results.Created($"api/evidence/files/{evidence.Id}", evidence) + return result.IsSuccess + ? Results.Created() : Results.BadRequest("Evidence upload failed"); }) .RequireAuthorization() @@ -70,7 +37,7 @@ public class UploadEvidenceFilesEndpoint : IEndpoint .WithDescription("Upload supporting evidence file to S3 and register metadata (M14)") .WithName(typeof(UploadEvidenceFilesEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) - .Produces(StatusCodes.Status201Created) + .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) .WithTags(EndpointTags.Evidence); diff --git a/PostFundManagement.Api/Extensions/Security.cs b/PostFundManagement.Api/Extensions/Security.cs index 3a16f10..472a0c3 100644 --- a/PostFundManagement.Api/Extensions/Security.cs +++ b/PostFundManagement.Api/Extensions/Security.cs @@ -1,7 +1,7 @@ using PostFundManagement.Domain.Api.Configuration; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Sdk; -using PostFundManagement.Domain.Services; +using PostFundManagement.Domain.Services.Shared; using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Api.Extensions; diff --git a/PostFundManagement.Api/PostFundManagement.Api.csproj b/PostFundManagement.Api/PostFundManagement.Api.csproj index d8d14d1..364db93 100644 --- a/PostFundManagement.Api/PostFundManagement.Api.csproj +++ b/PostFundManagement.Api/PostFundManagement.Api.csproj @@ -93,8 +93,7 @@ - - + diff --git a/PostFundManagement.Application/Communications/CommunicationsService.cs b/PostFundManagement.Application/Communications/CommunicationsService.cs new file mode 100644 index 0000000..14ba555 --- /dev/null +++ b/PostFundManagement.Application/Communications/CommunicationsService.cs @@ -0,0 +1,75 @@ +using PostFundManagement.Domain; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Infrastructure.Database; + +namespace PostFundManagement.Application.Communications; + +public sealed class CommunicationsService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask> SendInviteAsync(InviteRequest request, ClaimsPrincipal principal, CancellationToken cancellationToken = default) + { + try + { + var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value; + + if (!Guid.TryParse(sidClaim, out var currentUserId)) + return Result.Fail("Missing or invalid 'sid' claim."); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var invite = new Domain.Entities.Invite + { + OrganisationId = request.OrganisationId, + InvitedOrganisationId = request.InvitedOrganisationId, + AwardId = request.AwardId, + ContractId = request.ContractId, + Recipient = request.Recipient, + CreatedBy = currentUserId, + CreatedAt = DateTime.UtcNow + }; + + context.Invites.Add(invite); + + context.Notifications.Add(new Domain.Entities.Notification + { + OrganisationId = request.OrganisationId, + Recipient = request.Recipient, + Platform = request.Platform, + Status = NotificationStatus.Pending, + Subject = "Invitation to Onboard", + Message = request.Message, + CreatedAt = DateTime.UtcNow + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(invite.Map()) + : Result.Fail("Failed to complete the insite distribution"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var envites = await context.Invites.AsNoTracking() + .OrderByDescending(i => i.CreatedAt) + .Skip((pagination.Page - 1) * pagination.PageSize) + .Take(pagination.PageSize) + .ToListAsync(cancellationToken); + + return Result.Ok(envites.Select(i => i.Map()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/PostFundManagement.Application/Communications/Events/SyncUserEventHandler.cs b/PostFundManagement.Application/Communications/Events/SyncUserEventHandler.cs new file mode 100644 index 0000000..62d9d15 --- /dev/null +++ b/PostFundManagement.Application/Communications/Events/SyncUserEventHandler.cs @@ -0,0 +1,13 @@ +using PostFundManagement.Domain.Events.Communications; + +namespace PostFundManagement.Application.Communications.Events; + +public class SyncUserEventHandler(ILogger logger) : INotificationHandler +{ + public ValueTask Handle(SyncUserEvent notification, CancellationToken cancellationToken) + { + logger.LogInformation("Integration Sync executed. Correlation ID: {CorrelationId}", notification.CorrelationId); + + return ValueTask.CompletedTask; + } +} diff --git a/PostFundManagement.Application/Communications/Records.cs b/PostFundManagement.Application/Communications/Records.cs new file mode 100644 index 0000000..5d92003 --- /dev/null +++ b/PostFundManagement.Application/Communications/Records.cs @@ -0,0 +1,5 @@ +using PostFundManagement.Domain; + +namespace PostFundManagement.Application.Communications; + +public record InviteRequest(long OrganisationId, long InvitedOrganisationId, long AwardId, long ContractId, Guid Recipient, string Message, NotificationPlatform Platform); diff --git a/PostFundManagement.Application/Evidence/EvidenceService.cs b/PostFundManagement.Application/Evidence/EvidenceService.cs new file mode 100644 index 0000000..2456b51 --- /dev/null +++ b/PostFundManagement.Application/Evidence/EvidenceService.cs @@ -0,0 +1,251 @@ +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.Evidence; + +public sealed class EvidenceService(IDbContextFactory contextFactory, HashService hashService, + [FromKeyedServices(Constants.EvidenceS3SettingsSection)] IS3Service s3Service) : IService +{ + public async ValueTask UploadEvidenceFileAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) + { + try + { + _ = long.TryParse(form["projectId"], out var projectId); + _ = long.TryParse(form["milestoneId"], out var milestoneId); + _ = long.TryParse(form["indicatorId"], out var indicatorId); + _ = long.TryParse(form["activityId"], out var activityId); + _ = Enum.TryParse(form["type"], out var type); + + using var stream = file.OpenReadStream(); + + var uploadResult = await s3Service.UploadFileAsync(file.FileName, stream, file.ContentType, cancellationToken); + + if (uploadResult.IsFailed) + return Result.Fail(uploadResult.Errors.FirstOrDefault()?.Message ?? "File upload failed."); + + var documentUrl = uploadResult.Value; + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var evidence = new Domain.Entities.Evidence + { + ProjectId = projectId, + MilestoneId = milestoneId, + IndicatorId = indicatorId, + ActivityId = activityId, + CreatedBy = userId, + CreatedAt = DateTime.UtcNow, + Version = 1, + Type = type, + DocumentUrl = documentUrl, + Status = ApprovalStatus.Pending + }; + + context.Evidences.Add(evidence); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Evidence upload failed"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask RecordIndicatorActualAmountAsync(string hash, decimal amount, ClaimsPrincipal principal, CancellationToken cancellationToken = default) + { + try + { + var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value; + + if (!Guid.TryParse(sidClaim, out var userId)) + return Result.Fail("Missing or invalid 'sid' claim."); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var hashResult = hashService.DecodeLongIdHash(hash); + long indicatorId = 0; + + if (hashResult.IsFailed) + return Result.Fail(hashResult.Errors); + + var totalUpdated = await context.Indicators.Where(i => i.Id == indicatorId) + .ExecuteUpdateAsync(f => f + .SetProperty(f => f.ActualAmount, amount) + .SetProperty(f => f.CreatedBy, userId), cancellationToken); + + return totalUpdated > 0 + ? Result.Ok() + : Result.Fail("Failed to record actual amount"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetMilestonesByProjectIdAsync(long projectId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var milestones = await context.Milestones.AsNoTracking() + .Where(m => m.ProjectId == projectId) + .OrderBy(m => m.DueAt) + .ToListAsync(cancellationToken); + + return Result.Ok(milestones.Select(m => m.Map()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) + { + try + { + 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 milestones?.Count > 0 + ? Result.Ok(milestones.Select(i => i.Map()).ToArray()) + : Result.Ok([]); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var indicators = await context.Indicators.AsNoTracking() + .OrderByDescending(i => i.CreatedAt) + .Skip((pagination.Page - 1) * pagination.PageSize) + .Take(pagination.PageSize) + .ToListAsync(cancellationToken); + + return indicators?.Count > 0 + ? Result.Ok(indicators.Select(i => i.Map()).ToArray()) + : Result.Ok([]); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var evidence = await context.Evidences.AsNoTracking() + .OrderByDescending(e => e.CreatedAt) + .Skip((pagination.Page - 1) * pagination.PageSize) + .Take(pagination.PageSize) + .ToListAsync(cancellationToken); + + return evidence?.Count > 0 + ? Result.Ok(evidence.Select(e => e.Map()).ToArray()) + : Result.Ok([]); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateMilestoneAsync(CreateMilestoneRequest request, ClaimsPrincipal principal, CancellationToken cancellationToken = default) + { + try + { + var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value; + + if (!Guid.TryParse(sidClaim, out var userId)) + return Result.Fail("Missing or invalid 'sid' claim."); + + 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 milestone = new Domain.Entities.Milestone + { + ProjectId = request.ProjectId, + DueAt = request.DueAt, + Name = request.Name, + Priority = request.Priority, + Status = ApprovalStatus.Pending, + CreatedBy = userId, + CreatedAt = DateTime.UtcNow + }; + + context.Milestones.Add(milestone); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok((hashService.HashEncodeLongId(milestone.Id).Value, milestone.Map())) + : Result.Fail("Failed to create the milestone"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateIndicatorAsync(CreateIndicatorRequest request, ClaimsPrincipal principal, CancellationToken cancellationToken = default) + { + try + { + var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value; + + if (!Guid.TryParse(sidClaim, out var userId)) + return Result.Fail("Missing or invalid 'sid' claim."); + + 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 indicator = new Domain.Entities.Indicator + { + ProjectId = request.ProjectId, + Name = request.Name, + UnitOfMeasure = request.UnitOfMeasure, + BaselineAmount = request.BaselineAmount, + TargetAmount = request.TargetAmount, + ActualAmount = request.BaselineAmount, + CreatedBy = userId, + CreatedAt = DateTime.UtcNow + }; + + context.Indicators.Add(indicator); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok((hashService.HashEncodeLongId(indicator.Id).Value, indicator.Map())) + : Result.Fail("Failed to create the invite"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/PostFundManagement.Application/Evidence/Records.cs b/PostFundManagement.Application/Evidence/Records.cs new file mode 100644 index 0000000..3b12b61 --- /dev/null +++ b/PostFundManagement.Application/Evidence/Records.cs @@ -0,0 +1,9 @@ +using PostFundManagement.Domain; + +namespace PostFundManagement.Application.Evidence; + +public record RecordActualRequest(decimal ActualAmount); + +public record CreateMilestoneRequest(long ProjectId, DateTime DueAt, string Name, Priority Priority); + +public record CreateIndicatorRequest(long ProjectId, string Name, UnitOfMeasure UnitOfMeasure, decimal BaselineAmount, decimal TargetAmount); diff --git a/PostFundManagement.Application/PostFundManagement.Application.csproj b/PostFundManagement.Application/PostFundManagement.Application.csproj new file mode 100644 index 0000000..c50aa10 --- /dev/null +++ b/PostFundManagement.Application/PostFundManagement.Application.csproj @@ -0,0 +1,67 @@ + + + + net10.0 + enable + enable + ..\PostFundManagement.snk + True + ..\LICENSE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostFundManagement.Domain/Services/BrowserLocalStorageService.cs b/PostFundManagement.Application/Shared/BrowserLocalStorageService.cs similarity index 97% rename from PostFundManagement.Domain/Services/BrowserLocalStorageService.cs rename to PostFundManagement.Application/Shared/BrowserLocalStorageService.cs index 4ba6015..9004705 100644 --- a/PostFundManagement.Domain/Services/BrowserLocalStorageService.cs +++ b/PostFundManagement.Application/Shared/BrowserLocalStorageService.cs @@ -1,4 +1,4 @@ -namespace PostFundManagement.Domain.Services; +namespace PostFundManagement.Application.Shared; public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage) { diff --git a/PostFundManagement.Domain/Services/ContractS3Service.cs b/PostFundManagement.Application/Shared/ContractS3Service.cs similarity index 91% rename from PostFundManagement.Domain/Services/ContractS3Service.cs rename to PostFundManagement.Application/Shared/ContractS3Service.cs index 481f37e..d2ae490 100644 --- a/PostFundManagement.Domain/Services/ContractS3Service.cs +++ b/PostFundManagement.Application/Shared/ContractS3Service.cs @@ -1,7 +1,7 @@ using PostFundManagement.Domain.Abstractions; using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Services; +namespace PostFundManagement.Application.Shared; public sealed class ContractS3Service(IConfiguration configuration, [FromKeyedServices(ContractBucketName)] IAmazonS3 amazonS3) : S3ServiceBase(amazonS3), IS3Service diff --git a/PostFundManagement.Domain/Services/EmailService.cs b/PostFundManagement.Application/Shared/EmailService.cs similarity index 98% rename from PostFundManagement.Domain/Services/EmailService.cs rename to PostFundManagement.Application/Shared/EmailService.cs index 0a7610c..64106fc 100644 --- a/PostFundManagement.Domain/Services/EmailService.cs +++ b/PostFundManagement.Application/Shared/EmailService.cs @@ -1,10 +1,9 @@ -using System.Diagnostics; +using PostFundManagement.Domain; using PostFundManagement.Domain.Configuration.Email; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models.Email; -using static PostFundManagement.Domain.Extensions.EmailTelemetry; -namespace PostFundManagement.Domain.Services; +namespace PostFundManagement.Application.Shared; public sealed class EmailService(IOptions options) : IDisposable { diff --git a/PostFundManagement.Domain/Services/EvidenceS3Service.cs b/PostFundManagement.Application/Shared/EvidenceS3Service.cs similarity index 91% rename from PostFundManagement.Domain/Services/EvidenceS3Service.cs rename to PostFundManagement.Application/Shared/EvidenceS3Service.cs index 67de5ac..08948da 100644 --- a/PostFundManagement.Domain/Services/EvidenceS3Service.cs +++ b/PostFundManagement.Application/Shared/EvidenceS3Service.cs @@ -1,7 +1,7 @@ using PostFundManagement.Domain.Abstractions; using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Services; +namespace PostFundManagement.Application.Shared; public sealed class EvidenceS3Service(IConfiguration configuration, [FromKeyedServices(EvidenceBucketName)] IAmazonS3 amazonS3) : S3ServiceBase(amazonS3), IS3Service diff --git a/PostFundManagement.Domain/Services/GeneralS3Service.cs b/PostFundManagement.Application/Shared/GeneralS3Service.cs similarity index 91% rename from PostFundManagement.Domain/Services/GeneralS3Service.cs rename to PostFundManagement.Application/Shared/GeneralS3Service.cs index 79b0fb6..4ffcc2a 100644 --- a/PostFundManagement.Domain/Services/GeneralS3Service.cs +++ b/PostFundManagement.Application/Shared/GeneralS3Service.cs @@ -1,7 +1,7 @@ using PostFundManagement.Domain.Abstractions; using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Services; +namespace PostFundManagement.Application.Shared; public sealed class GeneralS3Service(IConfiguration configuration, [FromKeyedServices(GeneralBucketName)] IAmazonS3 amazonS3) : S3ServiceBase(amazonS3), IS3Service diff --git a/PostFundManagement.Domain/Services/HashService.cs b/PostFundManagement.Application/Shared/HashService.cs similarity index 98% rename from PostFundManagement.Domain/Services/HashService.cs rename to PostFundManagement.Application/Shared/HashService.cs index fc36262..8e5bafe 100644 --- a/PostFundManagement.Domain/Services/HashService.cs +++ b/PostFundManagement.Application/Shared/HashService.cs @@ -1,6 +1,6 @@ using PostFundManagement.Domain.Abstractions; -namespace PostFundManagement.Domain.Services; +namespace PostFundManagement.Application.Shared; public sealed partial class HashService(IHashids hasher) : IService { diff --git a/PostFundManagement.Domain/Services/TokenService.cs b/PostFundManagement.Application/Shared/TokenService.cs similarity index 80% rename from PostFundManagement.Domain/Services/TokenService.cs rename to PostFundManagement.Application/Shared/TokenService.cs index d4fcc56..d4ee12d 100644 --- a/PostFundManagement.Domain/Services/TokenService.cs +++ b/PostFundManagement.Application/Shared/TokenService.cs @@ -2,17 +2,17 @@ using PostFundManagement.Domain.Api.Configuration; using PostFundManagement.Domain.Api.Models; using PostFundManagement.Domain.Sdk; -namespace PostFundManagement.Domain.Services; +namespace PostFundManagement.Application.Shared; public sealed class TokenService(ISecurityConnectApi connectApi, IOptions clientOptions) { private readonly SecurityClientSettings clientSettings = clientOptions.Value; - public async Task> GenerateAsync(CancellationToken cancellationToken = default) + public async Task> GenerateAsync(CancellationToken cancellationToken = default) { try { - var request = new Api.Models.TokenRequest + var request = new Domain.Api.Models.TokenRequest { ClientId = clientSettings.ClientId, ClientSecret = clientSettings.ClientSecret, @@ -29,11 +29,11 @@ public sealed class TokenService(ISecurityConnectApi connectApi, IOptions(contentRaw); + var tokenResponse = JsonSerializer.Deserialize(contentRaw); return !string.IsNullOrWhiteSpace(tokenResponse?.AccessToken) ? Result.Ok(tokenResponse) - : Result.Fail(new Error("Authentication succeeded, but no access token was found in the response payload.")); + : Result.Fail(new Error("Authentication succeeded, but no access token was found in the response payload.")); } try diff --git a/PostFundManagement.Domain/Events/Communications/SyncUserEvent.cs b/PostFundManagement.Domain/Events/Communications/SyncUserEvent.cs new file mode 100644 index 0000000..b464d51 --- /dev/null +++ b/PostFundManagement.Domain/Events/Communications/SyncUserEvent.cs @@ -0,0 +1,8 @@ +using PostFundManagement.Domain.Abstractions; + +namespace PostFundManagement.Domain.Events.Communications; + +public class SyncUserEvent : EventBase, IEvent +{ + public string Name { get; set; } = nameof(SyncUserEvent); +} diff --git a/PostFundManagement.Domain/Extensions/Email.cs b/PostFundManagement.Domain/Extensions/Email.cs index 3e2da51..0caf536 100644 --- a/PostFundManagement.Domain/Extensions/Email.cs +++ b/PostFundManagement.Domain/Extensions/Email.cs @@ -1,5 +1,5 @@ using PostFundManagement.Domain.Configuration.Email; -using PostFundManagement.Domain.Services; +using PostFundManagement.Domain.Services.Shared; namespace PostFundManagement.Domain.Extensions; diff --git a/PostFundManagement.Domain/Extensions/General.cs b/PostFundManagement.Domain/Extensions/General.cs new file mode 100644 index 0000000..9c74f96 --- /dev/null +++ b/PostFundManagement.Domain/Extensions/General.cs @@ -0,0 +1,13 @@ +namespace PostFundManagement.Domain.Extensions; + +public static class General +{ + public static ProblemDetails ToProblems(this IReadOnlyList errors, string title) => new() + { + Errors = errors.Select(e => new KeyValuePair(e.Message, + [.. e.Reasons.Select(r => r.Message)])).ToDictionary(), + Detail = errors.FirstOrDefault()?.Message, + Status = StatusCodes.Status400BadRequest, + Title = title, + }; +} diff --git a/PostFundManagement.Domain/Extensions/S3.cs b/PostFundManagement.Domain/Extensions/S3.cs index add63c4..cbf0583 100644 --- a/PostFundManagement.Domain/Extensions/S3.cs +++ b/PostFundManagement.Domain/Extensions/S3.cs @@ -1,5 +1,5 @@ using PostFundManagement.Domain.Abstractions; -using PostFundManagement.Domain.Services; +using PostFundManagement.Domain.Services.Shared; using static PostFundManagement.Domain.Extensions.Constants; namespace PostFundManagement.Domain.Extensions; diff --git a/PostFundManagement.Domain/Pagination.cs b/PostFundManagement.Domain/Pagination.cs new file mode 100644 index 0000000..dfb78f4 --- /dev/null +++ b/PostFundManagement.Domain/Pagination.cs @@ -0,0 +1,41 @@ +namespace PostFundManagement.Domain; + +public sealed class Pagination +{ + const int defaultPage = 1; + const int defaultPageSize = 100; + const long defaultIndex = 0; + + public long Index { get; set; } + + public int Page { get; set; } + + public int PageSize { get; set; } + + public Pagination() + { + Page = defaultPage; + PageSize = defaultPageSize; + Index = defaultIndex; + } + + public Pagination(int page = defaultPage, int pageSize = defaultPageSize, long index = defaultIndex) + { + if (page < 1) page = defaultPage; + + if (pageSize < 1 || pageSize > 100) pageSize = defaultPageSize; + + Page = page; + PageSize = pageSize; + Index = index; + } + + public static Pagination Create(int page = defaultPage, int pageSize = defaultPageSize, long index = defaultIndex) + { + if (page < 1) page = defaultPage; + + if (pageSize < 1 || pageSize > 100) pageSize = defaultPageSize; + + return new(page, pageSize, index); + } +} diff --git a/PostFundManagement.slnx b/PostFundManagement.slnx index 65de65f..1438ccd 100644 --- a/PostFundManagement.slnx +++ b/PostFundManagement.slnx @@ -1,5 +1,6 @@ +