using FastEndpoints; using Microsoft.EntityFrameworkCore; using Socialize.Api.Data; using Socialize.Api.Infrastructure.Security; using Socialize.Api.Modules.Approvals.Services; using Socialize.Api.Modules.ContentItems.Data; using Socialize.Api.Modules.Notifications.Contracts; using Socialize.Api.Modules.Workspaces.Data; namespace Socialize.Api.Modules.ContentItems.Handlers; public record UpdateContentItemStatusRequest(string Status); public class UpdateContentItemStatusRequestValidator : Validator { public UpdateContentItemStatusRequestValidator() { RuleFor(x => x.Status).NotEmpty().MaximumLength(64); } } public class UpdateContentItemStatusHandler( AppDbContext dbContext, AccessScopeService accessScopeService, ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService, INotificationEventWriter notificationEventWriter) : Endpoint { private static readonly HashSet AllowedStatuses = [ "Draft", "In production", "In approval", "Approved", "Scheduled", "Published", ]; public override void Configure() { Post("/api/content-items/{id}/status"); Options(o => o.WithTags("Content Items")); } public override async Task HandleAsync(UpdateContentItemStatusRequest request, CancellationToken ct) { Guid id = Route("id"); ContentItem? item = await dbContext.ContentItems.SingleOrDefaultAsync(candidate => candidate.Id == id, ct); if (item is null) { await SendNotFoundAsync(ct); return; } if (!await accessScopeService.CanManageWorkspaceAsync(User, item.WorkspaceId, ct)) { await SendForbiddenAsync(ct); return; } string normalizedStatus = request.Status.Trim(); if (!AllowedStatuses.Contains(normalizedStatus)) { AddError(request => request.Status, "The requested status is not valid."); await SendErrorsAsync(StatusCodes.Status400BadRequest, ct); return; } Workspace? workspace = await dbContext.Workspaces.SingleOrDefaultAsync(candidate => candidate.Id == item.WorkspaceId, ct); if (workspace is null) { await SendNotFoundAsync(ct); return; } if (normalizedStatus == "In approval" && workspace.ApprovalMode == ApprovalModes.MultiLevel) { ApprovalWorkflowStartResult startResult = await approvalWorkflowRuntimeService.StartMultiLevelWorkflowAsync( item, workspace, User.GetUserId(), ct); if (!startResult.Succeeded) { AddError(request => request.Status, startResult.ErrorMessage ?? "The approval workflow could not be started."); await SendErrorsAsync(StatusCodes.Status409Conflict, ct); return; } } else if (ApprovalWorkflowRules.IsApprovalCompletionStatus(normalizedStatus) && ApprovalWorkflowRules.BlocksManualApprovedOrScheduledStatus(workspace.ApprovalMode)) { if (workspace.ApprovalMode == ApprovalModes.MultiLevel) { bool hasCompletedWorkflow = await approvalWorkflowRuntimeService.HasCompletedMultiLevelWorkflowAsync(item.Id, ct); if (!hasCompletedWorkflow) { AddError(request => request.Status, "This workspace requires the multi-level approval workflow to complete before content can be approved or scheduled."); await SendErrorsAsync(StatusCodes.Status409Conflict, ct); return; } } else { bool hasApprovedDecision = await dbContext.ApprovalRequests.AnyAsync( approval => approval.ContentItemId == item.Id && approval.WorkspaceId == item.WorkspaceId && approval.State == "Approved" && approval.CompletedAt.HasValue, ct); if (!hasApprovedDecision) { AddError(request => request.Status, "This workspace requires approval before content can be approved or scheduled."); await SendErrorsAsync(StatusCodes.Status409Conflict, ct); return; } } } if (item.Status != "In approval" || normalizedStatus != "In approval") { item.Status = normalizedStatus; } await dbContext.SaveChangesAsync(ct); await notificationEventWriter.WriteAsync( new NotificationEventWriteModel( item.WorkspaceId, item.Id, "content-item.status.updated", "ContentItem", item.Id, $"Status changed to {item.Status} for {item.Title}.", User.GetUserId(), User.GetEmail(), $$"""{"status":"{{item.Status}}"}"""), ct); ContentItemDetailDto dto = new( item.Id, item.WorkspaceId, item.ClientId, item.CampaignId, item.Title, item.PublicationMessage, item.PublicationTargets, item.Hashtags, item.Status, item.DueDate, item.CurrentRevisionLabel, item.CurrentRevisionNumber, item.CreatedAt); await SendOkAsync(dto, ct); } }