using Socialize.Infrastructure.Security; using Socialize.Modules.Notifications.Contracts; namespace Socialize.Modules.ContentItems.Handlers; public record CreateContentItemRequest( Guid WorkspaceId, Guid ClientId, Guid ProjectId, string Title, string PublicationMessage, string PublicationTargets, string? Hashtags, DateTimeOffset? DueDate); public class CreateContentItemRequestValidator : Validator { public CreateContentItemRequestValidator() { RuleFor(x => x.WorkspaceId).NotEmpty(); RuleFor(x => x.ClientId).NotEmpty(); RuleFor(x => x.ProjectId).NotEmpty(); RuleFor(x => x.Title).NotEmpty().MaximumLength(256); RuleFor(x => x.PublicationMessage).NotEmpty().MaximumLength(4000); RuleFor(x => x.PublicationTargets).NotEmpty().MaximumLength(512); RuleFor(x => x.Hashtags).MaximumLength(1024); } } public class CreateContentItemHandler( AppDbContext dbContext, AccessScopeService accessScopeService, INotificationEventWriter notificationEventWriter) : Endpoint { public override void Configure() { Post("/api/content-items"); Options(o => o.WithTags("Content Items")); } public override async Task HandleAsync(CreateContentItemRequest request, CancellationToken ct) { if (!accessScopeService.CanContributeToProject(User, request.WorkspaceId, request.ClientId, request.ProjectId)) { await SendForbiddenAsync(ct); return; } bool workspaceExists = await dbContext.Workspaces .AnyAsync(workspace => workspace.Id == request.WorkspaceId, ct); if (!workspaceExists) { AddError(request => request.WorkspaceId, "The selected workspace does not exist."); await SendErrorsAsync(StatusCodes.Status400BadRequest, ct); return; } bool clientExists = await dbContext.Clients .AnyAsync( client => client.Id == request.ClientId && client.WorkspaceId == request.WorkspaceId, ct); if (!clientExists) { AddError(request => request.ClientId, "The selected client does not belong to the active workspace."); await SendErrorsAsync(StatusCodes.Status400BadRequest, ct); return; } bool projectExists = await dbContext.Projects .AnyAsync( project => project.Id == request.ProjectId && project.WorkspaceId == request.WorkspaceId && project.ClientId == request.ClientId, ct); if (!projectExists) { AddError(request => request.ProjectId, "The selected project does not belong to the selected client."); await SendErrorsAsync(StatusCodes.Status400BadRequest, ct); return; } ContentItem item = new() { Id = Guid.NewGuid(), WorkspaceId = request.WorkspaceId, ClientId = request.ClientId, ProjectId = request.ProjectId, Title = request.Title.Trim(), PublicationMessage = request.PublicationMessage.Trim(), PublicationTargets = request.PublicationTargets.Trim(), Hashtags = string.IsNullOrWhiteSpace(request.Hashtags) ? null : request.Hashtags.Trim(), Status = "Draft", DueDate = request.DueDate, CurrentRevisionLabel = "v1", CurrentRevisionNumber = 1, CreatedAt = DateTimeOffset.UtcNow, }; dbContext.ContentItems.Add(item); dbContext.ContentItemRevisions.Add(new ContentItemRevision { Id = Guid.NewGuid(), ContentItemId = item.Id, RevisionNumber = 1, RevisionLabel = "v1", Title = item.Title, PublicationMessage = item.PublicationMessage, PublicationTargets = item.PublicationTargets, Hashtags = item.Hashtags, CreatedAt = DateTimeOffset.UtcNow, }); await dbContext.SaveChangesAsync(ct); await notificationEventWriter.WriteAsync( new NotificationEventWriteModel( item.WorkspaceId, item.Id, "content-item.created", "ContentItem", item.Id, $"Content item {item.Title} was created.", null, null, $$"""{"status":"{{item.Status}}","revisionLabel":"{{item.CurrentRevisionLabel}}"}"""), ct); ContentItemDto dto = new( item.Id, item.WorkspaceId, item.ClientId, item.ProjectId, item.Title, item.PublicationMessage, item.PublicationTargets, item.Hashtags, item.Status, item.DueDate, item.CurrentRevisionLabel, item.CurrentRevisionNumber); await SendAsync(dto, StatusCodes.Status201Created, ct); } }