using System.ComponentModel.DataAnnotations; using AdminDAL.CustomCode.FormTemplate; using AdminDAL.DTO.FormTemplate; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using GB5Shared.Validation; using Microsoft.Extensions.Logging; using static GB5Shared.GB5Constant.Constant; namespace AdminBLL.FormTemplate { // GB5-native rebuild of the legacy GB4 AdminBLL.FormTemplate. The legacy source // (~/Downloads/GB4Solution/BLL/AdminBLL/FormTemplate/FormTemplate.cs) is reference-only — // see /Users/venkatv/.claude/plans/we-have-done-a-adaptive-stroustrup.md for the corrected // findings this rebuild is based on (both storage strategies and D_ table // auto-creation are real on the legacy side; this class reimplements that behavior with // parameterized/identifier-validated SQL instead of raw string concatenation). // // NOT carried forward: legacy FormTemplate.Save() also auto-provisions MMenu/RoleVsMenu/ // MenuSetDetail rows via AutoEntriesinTables() whenever a parent module/menu is set. That is // an open decision (bigger blast radius than a typical BLL save) flagged in the plan rather // than assumed — the real MFORMTEMPLATE table (confirmed against a live GB5 dev database) // has no ParentModuleId/ParentMenuId columns at all, so there is nothing to persist toward // that feature yet; it would need a schema change if/when the decision is made. public class FormTemplateBLL : IFormTemplateBLL { private readonly IFormTemplateDAL _DAL; private readonly IValidation _Validation; private readonly EventLogPublish _EventLog; private readonly ILogger _Logger; public FormTemplateBLL( IFormTemplateDAL DAL, IValidation Validation, EventLogPublish EventLog, ILogger Logger) { _DAL = DAL; _Validation = Validation; _EventLog = EventLog; _Logger = Logger; } public async Task GetFormTemplate(int FormTemplateId, LoginDTO LoginDTO, CancellationToken ct) { GB5Trace.Step("get-formtemplate", new { FormTemplateId }); return await _DAL.GetFormTemplate(FormTemplateId, LoginDTO, ct).ConfigureAwait(false); } public async Task GetSelectListFormTemplate(CriteriaDTO CriteriaDTO, LoginDTO LoginDTO, CancellationToken ct) => await _DAL.GetSelectListFormTemplate(CriteriaDTO, LoginDTO, ct).ConfigureAwait(false); public async Task SaveFormTemplate(FormTemplateDTO FormTemplateDTO, LoginDTO LoginDTO, CancellationToken ct) { ArgumentNullException.ThrowIfNull(FormTemplateDTO); var isNew = FormTemplateDTO.FormTemplateId <= 0; GB5Trace.Step("validate-formtemplate", new { FormTemplateDTO.FormTemplateCode }); try { await _Validation.NotEmpty(FormTemplateDTO.FormTemplateCode, nameof(FormTemplateDTO.FormTemplateCode)).ConfigureAwait(false); await _Validation.NotEmpty(FormTemplateDTO.FormTemplateName, nameof(FormTemplateDTO.FormTemplateName)).ConfigureAwait(false); if (isNew) { var existing = await _DAL.GetFormTemplateByCode(FormTemplateDTO.FormTemplateCode, LoginDTO, ct).ConfigureAwait(false); if (existing is not null) throw new ValidationException( $"FormTemplateCode '{FormTemplateDTO.FormTemplateCode}' is already in use by another form."); } FormTemplateDTO.FormTemplateModifiedById = LoginDTO.UserId; FormTemplateDTO.FormTemplateModifiedOn = DateTime.UtcNow; // Real TENANTID column on MFORMTEMPLATE — resolved server-side, never trusted // from the client, same tenant-filtering rule as every other GB5 table. FormTemplateDTO.TenantId = LoginDTO.ClientId; GB5Trace.Step("save-formtemplate", new { FormTemplateDTO.FormTemplateId, isNew }); if (isNew) { FormTemplateDTO.FormTemplateCreatedById = LoginDTO.UserId; FormTemplateDTO.FormTemplateCreatedOn = DateTime.UtcNow; FormTemplateDTO.FormTemplateStatus = 1; // MFORMTEMPLATE.SOURCETYPE has a real CHECK constraint (SOURCETYPE IN // (1,2,3,4,5), confirmed live) — populated from the session's own // LoginDTO.SourceType, never left at a client-supplied/default value. FormTemplateDTO.FormTemplateSourceType = LoginDTO.SourceType; // FORMTEMPLATEID is a real IDENTITY column (confirmed against a live // database) — the DB assigns it, not an AutoNumber allocation. FormTemplateDTO.FormTemplateId = await _DAL.SaveFormTemplate(FormTemplateDTO, LoginDTO, ct).ConfigureAwait(false); } else { await _DAL.UpdateFormTemplate(FormTemplateDTO, LoginDTO, ct).ConfigureAwait(false); } // "Dynamic Table" (genericTable) forms get/keep a matching D_ table — // runs on every save (not just creation) so newly-authored fields become new // columns too, matching the legacy behavior this replaces. if (FormTemplateDTO.FormTemplateDataTableType == 1) { GB5Trace.Step("create-or-alter-dynamic-table", new { FormTemplateDTO.FormTemplateCode }); await _DAL.CreateOrAlterDynamicTable(FormTemplateDTO, LoginDTO, ct).ConfigureAwait(false); } GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.SAVEFORMTEMPLATEEVENTTYPEID }); await _EventLog.PublishEventLogAsync( isNew ? "FormTemplate Created" : "FormTemplate Updated", FormTemplateDTO, isNew ? EventTypeConstant.SAVEFORMTEMPLATEEVENTTYPEID : EventTypeConstant.UPDATEFORMTEMPLATEEVENTTYPEID, FormTemplateDTO.FormTemplateId, LoginDTO, ct: ct).ConfigureAwait(false); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {FormTemplateDTO.FormTemplateId}" : $"{SuccessResponse.UpdateSuccessMessage} {FormTemplateDTO.FormTemplateId}"; } catch (ValidationException vex) { GB5Trace.MarkFailed("save-formtemplate-validation-failed", vex); throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("save-formtemplate-failed", ex); _Logger.LogError(ex, "SaveFormTemplate failed for FormTemplateCode {Code}", FormTemplateDTO.FormTemplateCode); // No AutoNumber rollback needed — FORMTEMPLATEID is a real IDENTITY column, so a // failed insert simply never assigns/persists an id; nothing was pre-allocated. throw; } } public async Task DeleteFormTemplate(int FormTemplateId, LoginDTO LoginDTO, CancellationToken ct) { GB5Trace.Step("delete-formtemplate", new { FormTemplateId }); try { var result = await _DAL.DeleteFormTemplate(FormTemplateId, LoginDTO, ct).ConfigureAwait(false); GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.DELETEFORMTEMPLATEEVENTTYPEID }); await _EventLog.PublishEventLogAsync( "FormTemplate Deleted", new { FormTemplateId }, EventTypeConstant.DELETEFORMTEMPLATEEVENTTYPEID, FormTemplateId, LoginDTO, ct: ct).ConfigureAwait(false); return result; } catch (Exception ex) { GB5Trace.MarkFailed("delete-formtemplate-failed", ex); _Logger.LogError(ex, "DeleteFormTemplate failed for FormTemplateId {Id}", FormTemplateId); throw; } } } }