using CMSDAL.CustomCode.Navigation; using CMSDAL.CustomCode.Site; using CMSDAL.DTO.Navigation; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.EventLogPublish; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using GB5Shared.Validation; using System.ComponentModel.DataAnnotations; using static GB5Shared.GB5Constant.Constant; namespace CMSBLL.Navigation; public class NavigationBLL : INavigationBLL { private readonly INavigationDAL _dal; private readonly ISiteDAL _siteDal; private readonly ICrossTenantGuard _guard; private readonly BaseEntityAppService _entityApp; private readonly EventLogPublish _eventLogPublish; public NavigationBLL( INavigationDAL dal, ISiteDAL siteDal, ICrossTenantGuard guard, BaseEntityAppService entityApp, EventLogPublish eventLogPublish) { _dal = dal; _siteDal = siteDal; _guard = guard; _entityApp = entityApp; _eventLogPublish = eventLogPublish; } public async Task GetNavigation(int navigationId, LoginDTO loginDTO, CancellationToken ct = default) { try { return await _dal.GetNavigation(navigationId, loginDTO, ct); } catch { throw; } } public async Task GetBySiteAsync(int siteId, LoginDTO loginDTO, CancellationToken ct = default) { try { return await _dal.GetBySiteAsync(siteId, loginDTO, ct); } catch { throw; } } public async Task SaveNavigation(NavigationDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { var isNew = dto.NavigationId == 0; var eventTypeId = isNew ? EventTypeConstant.SAVECMSNAVIGATIONEVENTTYPEID : EventTypeConstant.UPDATECMSNAVIGATIONEVENTTYPEID; try { GB5Trace.Step("validate-navigation", new { dto.NavigationId, isNew }); // NOT `dto.TenantId <= 0` — real GB5 tenant IDs are frequently large negative // numbers; only the literal -1 is the system-wide/not-applicable sentinel. See // SiteBLL.SaveSite's matching check for how this was caught. if (dto.TenantId == -1) throw new ValidationException("Navigation must belong to a specific tenant."); if (string.IsNullOrWhiteSpace(dto.NavigationCode)) throw new ValidationException("NavigationCode is required."); // Only a brand-new navigation attaches to a caller-supplied SiteId — without this // check, any tenant could attach a navigation to another tenant's Site by guessing // its SiteId. if (isNew) { var ownerTenantId = await _siteDal.GetSiteTenantIdAsync(dto.SiteId, loginDTO, ct); _guard.EnsureOwnedByTenant(ownerTenantId, loginDTO.ClientId, "Site", dto.SiteId); } dto.ModifiedById = loginDTO.UserId; dto.ModifiedOn = DateTime.UtcNow; if (isNew) { dto.CreatedById = loginDTO.UserId; dto.CreatedOn = DateTime.UtcNow; } GB5Trace.Step("save-navigation", new { dto.NavigationId, isNew }); await _entityApp.ExecuteSaveAsync( EntityConstant.OBJECTCMSNAVIGATION, eventTypeId, dto, loginDTO, persistFunc: async tx => isNew ? await _dal.SaveNavigation(dto, loginDTO, tx, ct).ConfigureAwait(false) : await _dal.UpdateNavigation(dto, loginDTO, tx, ct).ConfigureAwait(false), isNewEntity: isNew); GB5Trace.Step("event-publish", new { EventTypeId = eventTypeId }); await _eventLogPublish.PublishEventLogAsync( isNew ? "CMS Navigation Created" : "CMS Navigation Updated", dto, eventTypeId, dto.NavigationId, loginDTO, ct: ct); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.NavigationId}" : SuccessResponse.UpdateSuccess; } catch (ValidationException vex) { GB5Trace.MarkFailed("save-navigation-validation-failed", vex); throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("save-navigation-failed", ex); throw; } } public async Task DeleteNavigation(int navigationId, LoginDTO loginDTO, CancellationToken ct = default) { try { return await _dal.DeleteNavigation(navigationId, loginDTO, ct); } catch { throw; } } }