using FastEndpoints; using Microsoft.EntityFrameworkCore; using Socialize.Api.Data; using Socialize.Api.Infrastructure.Observability; using Socialize.Api.Infrastructure.Security; using Socialize.Api.Modules.ContentItems.Contracts; using Socialize.Api.Modules.Notifications.Contracts; using Socialize.Api.Modules.ContentItems.Data; using Socialize.Api.Modules.Workspaces.Data; using System.Text.Json; namespace Socialize.Api.Modules.ContentItems.Handlers; internal record CreateContentItemRequest( Guid WorkspaceId, Guid ClientId, Guid CampaignId, string Title, string PublicationMessage, string PublicationTargets, string? Hashtags, DateTimeOffset? DueDate); internal class CreateContentItemRequestValidator : Validator { public CreateContentItemRequestValidator() { RuleFor(x => x.WorkspaceId).NotEmpty(); RuleFor(x => x.ClientId).NotEmpty(); RuleFor(x => x.CampaignId).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); } } internal class CreateContentItemHandler( AppDbContext dbContext, AccessScopeService accessScopeService, IContentItemActivityWriter activityWriter, INotificationEventWriter notificationEventWriter, SocializeMetrics metrics) : 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 (!await accessScopeService.CanContributeToCampaignAsync(User, request.WorkspaceId, request.ClientId, request.CampaignId, ct)) { 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 campaignExists = await dbContext.Campaigns .AnyAsync( campaign => campaign.Id == request.CampaignId && campaign.WorkspaceId == request.WorkspaceId && campaign.ClientId == request.ClientId, ct); if (!campaignExists) { AddError(request => request.CampaignId, "The selected campaign 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, CampaignId = request.CampaignId, 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); metrics.RecordContentItemCreated(item.WorkspaceId); await activityWriter.WriteAsync( new ContentItemActivityWriteModel( item.WorkspaceId, item.Id, "content-item.created", "ContentItem", item.Id, $"Content item {item.Title} was created.", User.GetUserId(), User.GetEmail(), JsonSerializer.Serialize(new { status = item.Status, revisionLabel = item.CurrentRevisionLabel, dueDate = item.DueDate, publicationTargets = item.PublicationTargets, hashtags = item.Hashtags, })), 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.CampaignId, item.Title, item.PublicationMessage, item.PublicationTargets, item.Hashtags, item.Status, item.DueDate, item.CurrentRevisionLabel, item.CurrentRevisionNumber); await SendAsync(dto, StatusCodes.Status201Created, ct); } }