using CMSBLL.Content; using CMSDAL.CustomCode.Content; using CMSDAL.CustomCode.ContentVersion; using CMSDAL.DTO.Content; using CMSDAL.DTO.ContentVersion; using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.EventLogPublish; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.ResponseStandard; using GB5Shared.Telemetry; using GB5Shared.Validation; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using static GB5Shared.GB5Constant.Constant; namespace FrameworkBLL.Content { public class ContentBLL : IContentBLL { private readonly IContentDAL _ContentDAL; private readonly IContentVersionDAL _ContentVersionDAL; private readonly BaseEntityAppService _EntityApp; private readonly EventLogPublish _EventLogPublish; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; public ContentBLL( IContentDAL ContentDAL, IContentVersionDAL ContentVersionDAL, BaseEntityAppService EntityApp, EventLogPublish EventLogPublish, AutoNumber AutoNumber, IValidation Validation) { _ContentDAL = ContentDAL; _ContentVersionDAL = ContentVersionDAL; _EntityApp = EntityApp; _EventLogPublish = EventLogPublish; _AutoNumber = AutoNumber; _Validation = Validation; } public async Task GetContent(int ContentId, LoginDTO LoginDTO) { try { return await _ContentDAL.GetContent(ContentId, LoginDTO); } catch (Exception) { throw; } } public async Task GetContentBySlug(string Slug, LoginDTO LoginDTO) { if (string.IsNullOrWhiteSpace(Slug)) throw new ArgumentException("Slug must not be empty."); // Accept a pasted full URL as well as a bare slug — take the last non-empty // path segment as the candidate slug. string candidate = Slug.Trim(); if (candidate.Contains("://") || candidate.Contains("/")) { var segments = candidate.Split('/', StringSplitOptions.RemoveEmptyEntries); candidate = segments.Length > 0 ? segments[^1] : candidate; } return await _ContentDAL.GetContentBySlug(candidate, LoginDTO); } public async Task SaveContent(ContentDTO ContentDTO, LoginDTO LoginDTO, CancellationToken ct = default) { var isNew = ContentDTO.ContentId == 0; var eventTypeId = isNew ? EventTypeConstant.SAVECONTENTEVENTTYPEID : EventTypeConstant.UPDATECONTENTEVENTTYPEID; try { GB5Trace.Step("validate-content", new { ContentDTO.ContentId, isNew }); ContentDTO.ContentModifiedById = LoginDTO.UserId; ContentDTO.ContentModifiedOn = DateTime.UtcNow; if (isNew) { ContentDTO.ContentCreatedById = LoginDTO.UserId; ContentDTO.ContentCreatedOn = DateTime.UtcNow; } GB5Trace.Step("save-content", new { ContentDTO.ContentId, isNew }); await _EntityApp.ExecuteSaveAsync( EntityConstant.OBJECTCONTENT, eventTypeId, ContentDTO, LoginDTO, persistFunc: async tx => isNew ? await _ContentDAL.SaveContent(ContentDTO, LoginDTO, tx, ct).ConfigureAwait(false) : await _ContentDAL.UpdateContent(ContentDTO, LoginDTO, tx, ct).ConfigureAwait(false), isNewEntity: isNew); GB5Trace.Step("event-publish", new { EventTypeId = eventTypeId }); await _EventLogPublish.PublishEventLogAsync( isNew ? "CMS Content Created" : "CMS Content Updated", ContentDTO, eventTypeId, ContentDTO.ContentId, LoginDTO, ct: ct); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {ContentDTO.ContentId}" : SuccessResponse.UpdateSuccess; } catch (ValidationException vex) { GB5Trace.MarkFailed("save-content-validation-failed", vex); throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("save-content-failed", ex); throw; } } // Shared workflow-transition pipeline for SubmitForReviewAsync/ApproveContentAsync/ // PublishContentAsync/ArchiveContentAsync — each is a status flip on an existing Content // row that must (a) be validated against its current status precondition, (b) commit // atomically with a ContentVersion snapshot so rollback actually works, and (c) publish // through the same ExecuteSaveAsync/EventLogPublish pipeline as SaveContent. private async Task TransitionAsync( int contentId, byte requiredStatus, byte newStatus, bool usePublishUpdate, int eventTypeId, string stepPrefix, string preconditionErrorMessage, string eventText, LoginDTO loginDTO, CancellationToken ct) { try { GB5Trace.Step($"{stepPrefix}-validate", new { ContentId = contentId }); var currentStatus = await _ContentDAL.GetContentStatusAsync(contentId, loginDTO, ct: ct); if (currentStatus != requiredStatus) throw new ValidationException(preconditionErrorMessage); var snapshotJson = await _ContentDAL.GetContent(contentId, loginDTO); var dto = new ContentDTO { ContentId = contentId, ContentStatus = newStatus }; GB5Trace.Step($"{stepPrefix}-transition", new { ContentId = contentId, newStatus }); await _EntityApp.ExecuteSaveAsync( EntityConstant.OBJECTCONTENT, eventTypeId, dto, loginDTO, persistFunc: async tx => { if (usePublishUpdate) await _ContentDAL.UpdateContentStatusWithPublishAsync(contentId, newStatus, loginDTO, tx, ct).ConfigureAwait(false); else await _ContentDAL.UpdateContentStatusAsync(contentId, newStatus, loginDTO, tx, ct).ConfigureAwait(false); await _ContentVersionDAL.SaveWithAutoVersionAsync( new ContentVersionDTO { ContentId = contentId, SnapshotJson = snapshotJson, Notes = stepPrefix }, loginDTO, tx, ct).ConfigureAwait(false); return contentId; }, isNewEntity: false); GB5Trace.Step($"{stepPrefix}-event-publish", new { EventTypeId = eventTypeId }); await _EventLogPublish.PublishEventLogAsync(eventText, dto, eventTypeId, contentId, loginDTO, ct: ct); return SuccessResponse.UpdateSuccess; } catch (ValidationException vex) { GB5Trace.MarkFailed($"{stepPrefix}-validation-failed", vex); throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed($"{stepPrefix}-failed", ex); throw; } } public async Task GetSelectListContent(int FirstNumber, int MaxResult, CriteriaDTO CriteriaDTO, LoginDTO LoginDTO) { try { return await _ContentDAL.GetSelectListContent(FirstNumber, MaxResult, CriteriaDTO, LoginDTO); } catch (Exception) { throw; } } public async Task DeleteContent(int ContentId, LoginDTO LoginDTO) { try { return await _ContentDAL.DeleteContent(ContentId, LoginDTO); } catch (Exception) { throw; } } public async Task GetContentListAsync(int spaceId, int contentTypeId, int contentStatus, int authorId, DateTime? fromDate, DateTime? toDate, LoginDTO loginDTO) { try { return await _ContentDAL.GetContentListAsync(spaceId, contentTypeId, contentStatus, authorId, fromDate, toDate, loginDTO); } catch (Exception) { throw; } } public async Task SearchContentAsync(string searchText, int spaceId, int contentTypeId, LoginDTO loginDTO) { try { return await _ContentDAL.SearchContentAsync(searchText, spaceId, contentTypeId, loginDTO); } catch (Exception) { throw; } } public async Task SubmitForReviewAsync(int contentId, LoginDTO loginDTO, CancellationToken ct = default) => await TransitionAsync(contentId, requiredStatus: 0, newStatus: 1, usePublishUpdate: false, EventTypeConstant.SUBMITCONTENTFORREVIEWEVENTTYPEID, "submit-content-for-review", "Content must be in Draft status to submit for review.", "CMS Content Submitted For Review", loginDTO, ct); public async Task ApproveContentAsync(int contentId, LoginDTO loginDTO, CancellationToken ct = default) => await TransitionAsync(contentId, requiredStatus: 1, newStatus: 2, usePublishUpdate: false, EventTypeConstant.APPROVECONTENTEVENTTYPEID, "approve-content", "Content must be In Review status to approve.", "CMS Content Approved", loginDTO, ct); public async Task PublishContentAsync(int contentId, LoginDTO loginDTO, CancellationToken ct = default) => await TransitionAsync(contentId, requiredStatus: 2, newStatus: 3, usePublishUpdate: true, EventTypeConstant.PUBLISHCONTENTEVENTTYPEID, "publish-content", "Content must be Approved status to publish.", "CMS Content Published", loginDTO, ct); public async Task ArchiveContentAsync(int contentId, LoginDTO loginDTO, CancellationToken ct = default) => await TransitionAsync(contentId, requiredStatus: 3, newStatus: 4, usePublishUpdate: false, EventTypeConstant.ARCHIVECONTENTEVENTTYPEID, "archive-content", "Content must be Published status to archive.", "CMS Content Archived", loginDTO, ct); } }