using FrameworkDAL.CustomCode.DirectAction.DirectActionDetailAdmin; using FrameworkDAL.DTO.DirectAction; using GB5Shared.DTO.Framework.Login; using GB5Shared.Resource.Response; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace FrameworkBLL.DirectAction.DirectActionDetailAdmin { public class DirectActionDetailAdminBLL : IDirectActionDetailAdminBLL { private static readonly Regex ActionCodePattern = new(@"^[A-Za-z0-9_]{1,50}$", RegexOptions.Compiled); private static readonly string[] AllowedHttpMethods = { "POST", "PUT", "PATCH", "DELETE" }; private readonly IDirectActionDetailAdminDAL _dal; private readonly ILogger _logger; public DirectActionDetailAdminBLL(IDirectActionDetailAdminDAL dal, ILogger logger) { _dal = dal ?? throw new ArgumentNullException(nameof(dal)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task> GetDetailListAsync( int directActionId, LoginDTO login, CancellationToken ct = default) { try { _logger.LogInformation("BLL | GetDetailList | Tenant={TenantId} | DirectActionId={Id}", login.ClientId, directActionId); return await _dal.GetDetailListAsync(directActionId, login, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "BLL | GetDetailList | Error | DirectActionId={Id}", directActionId); throw; } } public async Task GetDetailAsync( int directActionDetailId, LoginDTO login, CancellationToken ct = default) { try { _logger.LogInformation("BLL | GetDetail | Tenant={TenantId} | Id={Id}", login.ClientId, directActionDetailId); return await _dal.GetDetailAsync(directActionDetailId, login, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "BLL | GetDetail | Error | Id={Id}", directActionDetailId); throw; } } public async Task SaveDetailAsync( DirectActionDetailAdminDTO dto, LoginDTO login, CancellationToken ct = default) { try { _logger.LogInformation( "BLL | SaveDetail | Validate | Tenant={TenantId} | ActionCode={Code}", login.ClientId, dto.ActionCode); ValidateActionCode(dto.ActionCode); if (string.IsNullOrWhiteSpace(dto.ActionLabel)) throw new ArgumentException("ActionLabel is required."); if (string.IsNullOrWhiteSpace(dto.ApiEndpoint)) throw new ArgumentException("ApiEndpoint is required."); if (string.IsNullOrWhiteSpace(dto.PayloadTemplate)) throw new ArgumentException("PayloadTemplate is required."); // AutoNumber-generated ids in this repo are negative — only 0 (C# default, // "never set") is genuinely invalid; matches EIPRoutingAdminBLL's FlowId check. if (dto.DirectActionId == 0) throw new ArgumentException("DirectActionId must reference a valid DirectAction group."); if (Array.IndexOf(AllowedHttpMethods, dto.HttpMethod) < 0) throw new ArgumentException("HttpMethod must be one of: POST, PUT, PATCH, DELETE."); if (dto.NeedsInput != 0 && dto.NeedsInput != 1) throw new ArgumentException("NeedsInput must be 0 (direct API call) or 1 (show remarks form)."); // ACTIONCODE is used in signed token payloads and looked up globally // (DirectActionConfigQB.GET_DETAIL_BY_ACTIONCODE has no DirectActionId filter). bool codeExists = await _dal.ActionCodeExistsAsync( dto.ActionCode, dto.DirectActionDetailId, login, ct).ConfigureAwait(false); if (codeExists) throw new InvalidOperationException($"ActionCode '{dto.ActionCode}' already exists."); var isNew = dto.DirectActionDetailId == 0; _logger.LogInformation( "BLL | SaveDetail | Persist | Tenant={TenantId} | ActionCode={Code} | IsNew={IsNew}", login.ClientId, dto.ActionCode, isNew); if (isNew) { var newId = await _dal.InsertDetailAsync(dto, login, ct).ConfigureAwait(false); _logger.LogInformation("BLL | SaveDetail | Inserted | NewId={NewId}", newId); return $"{SuccessResponse.SaveSuccessMessage} {newId}"; } else { await _dal.UpdateDetailAsync(dto, login, ct).ConfigureAwait(false); _logger.LogInformation("BLL | SaveDetail | Updated | Id={Id}", dto.DirectActionDetailId); return SuccessResponse.UpdateSuccess; } } catch (Exception ex) { _logger.LogError(ex, "BLL | SaveDetail | Error | Tenant={TenantId} | ActionCode={Code}", login.ClientId, dto.ActionCode); throw; } } public async Task DeleteDetailAsync( int directActionDetailId, LoginDTO login, CancellationToken ct = default) { try { _logger.LogInformation("BLL | DeleteDetail | Tenant={TenantId} | Id={Id}", login.ClientId, directActionDetailId); if (directActionDetailId == 0) throw new ArgumentException("DirectActionDetailId must reference a valid record."); await _dal.DeleteDetailAsync(directActionDetailId, login, ct).ConfigureAwait(false); _logger.LogInformation("BLL | DeleteDetail | Completed | Id={Id}", directActionDetailId); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { _logger.LogError(ex, "BLL | DeleteDetail | Error | Id={Id}", directActionDetailId); throw; } } private static void ValidateActionCode(string actionCode) { if (string.IsNullOrWhiteSpace(actionCode)) throw new ArgumentException("ActionCode is required."); if (!ActionCodePattern.IsMatch(actionCode)) throw new ArgumentException( "ActionCode must contain only alphanumeric characters and underscores (max 50 chars)."); } } }