using System; using System.Threading; using System.Threading.Tasks; using FluentResults; using MediatR; using Microsoft.EntityFrameworkCore; using NexusReader.Application.Abstractions.Messaging; using NexusReader.Data.Persistence; using NexusReader.Domain.Entities; using NexusReader.Domain.Exceptions; namespace NexusReader.Application.Features.Books.Commands; /// /// MediatR handler for publishing a Book version and setting up the next Working Draft. /// public class PublishBookVersionCommandHandler : ICommandHandler { private readonly IDbContextFactory _dbContextFactory; public PublishBookVersionCommandHandler(IDbContextFactory dbContextFactory) { _dbContextFactory = dbContextFactory; } public async Task Handle(PublishBookVersionCommand request, CancellationToken cancellationToken) { using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); // Fetch the Book including its CurrentDraftRevision and all associated Chapters, // enforcing that the book belongs to the requested TenantId and UserId to prevent cross-tenant data leaks. var book = await dbContext.Books .Include(b => b.CurrentDraftRevision) .ThenInclude(r => r!.Chapters) .FirstOrDefaultAsync( b => b.Id == request.BookId && b.UserId == request.UserId && b.TenantId == request.TenantId, cancellationToken); if (book == null) { throw new BookNotFoundException(request.BookId); } var oldDraftRevision = book.CurrentDraftRevision; if (oldDraftRevision == null) { return Result.Fail(new Error("The book does not have an active draft revision to publish.")); } // Start ACID transaction using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken); try { // 1. Update the current draft revision: Set IsPublished = true, PublishedAt = now, VersionString = custom oldDraftRevision.IsPublished = true; oldDraftRevision.PublishedAt = DateTime.UtcNow; oldDraftRevision.VersionString = request.CustomVersionString; // 2. Point the Book.LivePublishedRevisionId to this newly frozen revision ID book.LivePublishedRevisionId = oldDraftRevision.Id; // 3. Execute Deep Snapshot: Instantiate a brand new BookRevision representing the next "Working Draft" var newDraftRevision = new BookRevision { Id = Guid.NewGuid(), BookId = book.Id, VersionString = "Working Draft", IsPublished = false, CreatedAt = DateTime.UtcNow }; dbContext.BookRevisions.Add(newDraftRevision); // Replicate/clone chapters into new Chapter objects associated with the new draft revision. // Reset identities by explicitly instantiating completely new Chapter objects with Guid.NewGuid(). foreach (var oldChapter in oldDraftRevision.Chapters) { var newChapter = new Chapter { Id = Guid.NewGuid(), BookRevisionId = newDraftRevision.Id, Title = oldChapter.Title, MarkdownContent = oldChapter.MarkdownContent, SortOrder = oldChapter.SortOrder }; dbContext.Chapters.Add(newChapter); } // 4. Assign the new draft revision ID to Book.CurrentDraftRevisionId book.CurrentDraftRevisionId = newDraftRevision.Id; // Save changes and commit transaction await dbContext.SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); return Result.Ok(); } catch (Exception ex) { try { await transaction.RollbackAsync(cancellationToken); } catch (Exception rollbackEx) { Console.WriteLine($"[PublishBookVersion] Transaction rollback failed: {rollbackEx.Message}"); } return Result.Fail(new Error($"Failed to publish book version: {ex.Message}").CausedBy(ex)); } } }