using FastEndpoints; using Microsoft.EntityFrameworkCore; using Socialize.Api.Data; using Socialize.Api.Infrastructure.Security; using Socialize.Api.Modules.ContentItems.Contracts; using Socialize.Api.Modules.ContentItems.Data; using Socialize.Api.Modules.Notifications.Contracts; using System.Text.Json; namespace Socialize.Api.Modules.ContentItems.Handlers; public record CreateContentItemRevisionRequest( string Title, string PublicationMessage, string PublicationTargets, string? Hashtags, string? ChangeSummary, DateTimeOffset? DueDate); public class CreateContentItemRevisionRequestValidator : Validator { public CreateContentItemRevisionRequestValidator() { 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); RuleFor(x => x.ChangeSummary).MaximumLength(1024); } } public class CreateContentItemRevisionHandler( AppDbContext dbContext, AccessScopeService accessScopeService, IContentItemActivityWriter activityWriter, INotificationEventWriter notificationEventWriter) : Endpoint { public override void Configure() { Post("/api/content-items/{id}/revisions"); Options(o => o.WithTags("Content Items")); } public override async Task HandleAsync(CreateContentItemRevisionRequest 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.CanContributeToCampaignAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct)) { await SendForbiddenAsync(ct); return; } int revisionNumber = item.CurrentRevisionNumber + 1; string revisionLabel = $"v{revisionNumber}"; string previousTitle = item.Title; string previousPublicationMessage = item.PublicationMessage; string previousPublicationTargets = item.PublicationTargets; string? previousHashtags = item.Hashtags; DateTimeOffset? previousDueDate = item.DueDate; string newTitle = request.Title.Trim(); string newPublicationMessage = request.PublicationMessage.Trim(); string newPublicationTargets = request.PublicationTargets.Trim(); string? newHashtags = string.IsNullOrWhiteSpace(request.Hashtags) ? null : request.Hashtags.Trim(); item.Title = newTitle; item.PublicationMessage = newPublicationMessage; item.PublicationTargets = newPublicationTargets; item.Hashtags = newHashtags; item.DueDate = request.DueDate; item.CurrentRevisionNumber = revisionNumber; item.CurrentRevisionLabel = revisionLabel; ContentItemRevision revision = new() { Id = Guid.NewGuid(), ContentItemId = item.Id, RevisionNumber = revisionNumber, RevisionLabel = revisionLabel, Title = item.Title, PublicationMessage = item.PublicationMessage, PublicationTargets = item.PublicationTargets, Hashtags = item.Hashtags, ChangeSummary = string.IsNullOrWhiteSpace(request.ChangeSummary) ? null : request.ChangeSummary.Trim(), CreatedByUserId = User.GetUserId(), CreatedAt = DateTimeOffset.UtcNow, }; dbContext.ContentItemRevisions.Add(revision); await dbContext.SaveChangesAsync(ct); List changedFields = []; AddChangedField(changedFields, "title", previousTitle, item.Title); AddChangedField(changedFields, "publicationMessage", previousPublicationMessage, item.PublicationMessage); AddChangedField(changedFields, "publicationTargets", previousPublicationTargets, item.PublicationTargets); AddChangedField(changedFields, "hashtags", previousHashtags, item.Hashtags); AddChangedField(changedFields, "dueDate", previousDueDate, item.DueDate); await activityWriter.WriteAsync( new ContentItemActivityWriteModel( item.WorkspaceId, item.Id, "content-item.revision.created", "ContentItemRevision", revision.Id, $"Revision {revisionLabel} was created for {item.Title}.", User.GetUserId(), User.GetEmail(), JsonSerializer.Serialize(new { revisionLabel, revisionNumber, changeSummary = revision.ChangeSummary, changedFields, })), ct); await notificationEventWriter.WriteAsync( new NotificationEventWriteModel( item.WorkspaceId, item.Id, "content-item.revision.created", "ContentItemRevision", revision.Id, $"Revision {revisionLabel} was created for {item.Title}.", User.GetUserId(), User.GetEmail(), $$"""{"revisionLabel":"{{revisionLabel}}","status":"{{item.Status}}"}"""), ct); ContentItemRevisionDto dto = new( revision.Id, revision.ContentItemId, revision.RevisionNumber, revision.RevisionLabel, revision.Title, revision.PublicationMessage, revision.PublicationTargets, revision.Hashtags, revision.ChangeSummary, revision.CreatedByUserId, revision.CreatedAt); await SendAsync(dto, StatusCodes.Status201Created, ct); } private static void AddChangedField(List changedFields, string field, T oldValue, T newValue) { if (EqualityComparer.Default.Equals(oldValue, newValue)) { return; } changedFields.Add(new { field, oldValue, newValue, }); } }