using System.Text.Json; using FLSDAL.CustomCode.Survey; using FLSDAL.DTO.Survey; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; namespace FLSBLL.Survey; public class SurveyInstrumentBLL : ISurveyInstrumentBLL { private readonly ISurveyInstrumentDAL _dal; private readonly AutoNumber _autoNumber; private readonly ILogger _logger; public SurveyInstrumentBLL(ISurveyInstrumentDAL dal, AutoNumber autoNumber, ILogger logger) { _dal = dal; _autoNumber = autoNumber; _logger = logger; } public async Task GetInstrumentAsync(int flsRegistrationId, string langCode, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-survey-instrument", new { flsRegistrationId, langCode }); var instrumentConfigId = await _dal.GetConfigIdByRegistrationAsync(flsRegistrationId, login, ct) .ConfigureAwait(false); if (instrumentConfigId == -1) return "{}"; var dto = await BuildInstrumentDtoAsync(instrumentConfigId, langCode, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(dto); } catch (Exception ex) { GB5Trace.MarkFailed("get-survey-instrument-failed", ex); _logger.LogError(ex, "GetInstrument failed for regId {Id}", flsRegistrationId); throw; } } public async Task GetInstrumentPreviewAsync(int instrumentConfigId, string langCode, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-survey-preview", new { instrumentConfigId, langCode }); if (instrumentConfigId == -1) return "{}"; var dto = await BuildInstrumentDtoAsync(instrumentConfigId, langCode, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(dto); } catch (Exception ex) { GB5Trace.MarkFailed("get-survey-preview-failed", ex); _logger.LogError(ex, "GetInstrumentPreview failed for configId {Id}", instrumentConfigId); throw; } } public async Task LockInstrumentAsync(int instrumentConfigId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("lock-survey-instrument", new { instrumentConfigId }); await _dal.LockInstrumentAsync(instrumentConfigId, login, ct).ConfigureAwait(false); return "Instrument locked."; } catch (Exception ex) { GB5Trace.MarkFailed("lock-survey-instrument-failed", ex); _logger.LogError(ex, "LockInstrument failed for configId {Id}", instrumentConfigId); throw; } } public async Task> GetSelectListSurveyInstrumentType( int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO login) { try { if (firstNumber <= 0 && maxResult <= 0) { firstNumber = -1; maxResult = -1; } return await _dal.GetSelectListSurveyInstrumentType(firstNumber, maxResult, criteriaDTO, login).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-surveyinstrumenttype-failed", ex); _logger.LogError(ex, "GetSelectListSurveyInstrumentType failed"); throw; } } public async Task> GetSelectListSurveyQuestionType( int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO login) { try { if (firstNumber <= 0 && maxResult <= 0) { firstNumber = -1; maxResult = -1; } return await _dal.GetSelectListSurveyQuestionType(firstNumber, maxResult, criteriaDTO, login).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-surveyquestiontype-failed", ex); _logger.LogError(ex, "GetSelectListSurveyQuestionType failed"); throw; } } public async Task SaveInstrumentAsync(SaveSurveyInstrumentRequest req, LoginDTO login, CancellationToken ct) { try { // Quiz/Assessment roadmap Phase 2: MSURVEYINSTRUMENTTYPE's QUIZ/TEST rows are retired // (STATUS=2/Archived) — nothing downstream ever scored these as real quizzes, and TMS's // Quiz engine is the one real assessment engine now. Reject at save time rather than // silently accepting a dead-end survey type. var instrumentTypeStatus = await _dal.GetInstrumentTypeStatusAsync(req.InstrumentTypeId, login, ct).ConfigureAwait(false); if (instrumentTypeStatus == 2) throw new InvalidOperationException( "This instrument type has been retired. Quiz/Test delivery is handled by TMS's assessment engine — use the TMS Quiz APIs instead of an FLS survey."); int instrumentConfigId = req.InstrumentConfigId; // PK convention: InstrumentConfigId == 0 → insert (new AutoNumber); non-zero → update existing row. if (instrumentConfigId == 0) { var autoNum = await _autoNumber.GetNumberAsync(1, "SURVEYINSTRUMENTCONFIG", login).ConfigureAwait(false); instrumentConfigId = autoNum.StartNumber; await _dal.InsertInstrumentConfigAsync(instrumentConfigId, req, login, ct).ConfigureAwait(false); } else { bool updated = await _dal.UpdateInstrumentConfigAsync(instrumentConfigId, req, login, ct).ConfigureAwait(false); if (!updated) return $"Instrument {instrumentConfigId} not found."; } var keepSectionIds = new List(); foreach (var section in req.Sections) { int instrumentSectionId = section.InstrumentSectionId; if (instrumentSectionId == 0) { var autoNum = await _autoNumber.GetNumberAsync(1, "SURVEYINSTRUMENTSECTION", login).ConfigureAwait(false); instrumentSectionId = autoNum.StartNumber; await _dal.InsertSectionAsync(instrumentSectionId, instrumentConfigId, section, login, ct).ConfigureAwait(false); } else { await _dal.UpdateSectionAsync(instrumentSectionId, section, login, ct).ConfigureAwait(false); } keepSectionIds.Add(instrumentSectionId); var keepQuestionIds = new List(); foreach (var question in section.Questions) { int instQuestionId = question.InstQuestionId; if (instQuestionId == 0) { var autoNum = await _autoNumber.GetNumberAsync(1, "SURVEYINSTQUESTION", login).ConfigureAwait(false); instQuestionId = autoNum.StartNumber; await _dal.InsertInstQuestionAsync(instQuestionId, instrumentSectionId, instrumentConfigId, question, login, ct).ConfigureAwait(false); } else { await _dal.UpdateInstQuestionAsync(instQuestionId, question, login, ct).ConfigureAwait(false); } keepQuestionIds.Add(instQuestionId); } await _dal.DeactivateInstQuestionsNotInAsync(instrumentSectionId, keepQuestionIds, login, ct).ConfigureAwait(false); } await _dal.DeactivateSectionsNotInAsync(instrumentConfigId, keepSectionIds, login, ct).ConfigureAwait(false); return $"{SuccessResponse.SaveSuccessMessage} {instrumentConfigId}"; } catch (Exception ex) { GB5Trace.MarkFailed("save-survey-instrument-failed", ex); _logger.LogError(ex, "SaveInstrument failed for configId {Id}", req.InstrumentConfigId); throw; } } public async Task SaveSectionAsync(SaveSurveySectionOnlyRequest req, LoginDTO login, CancellationToken ct) { try { int instrumentSectionId = req.InstrumentSectionId; // PK convention: InstrumentSectionId == 0 → insert (new AutoNumber); non-zero → update existing row. if (instrumentSectionId == 0) { var autoNum = await _autoNumber.GetNumberAsync(1, "SURVEYINSTRUMENTSECTION", login).ConfigureAwait(false); instrumentSectionId = autoNum.StartNumber; await _dal.InsertSectionOnlyAsync(instrumentSectionId, req, login, ct).ConfigureAwait(false); } else { bool updated = await _dal.UpdateSectionOnlyAsync(instrumentSectionId, req, login, ct).ConfigureAwait(false); if (!updated) return $"Section {instrumentSectionId} not found."; } return $"{SuccessResponse.SaveSuccessMessage} {instrumentSectionId}"; } catch (Exception ex) { GB5Trace.MarkFailed("save-survey-section-failed", ex); _logger.LogError(ex, "SaveSection failed for sectionId {Id}", req.InstrumentSectionId); throw; } } public async Task DeleteSectionAsync(int instrumentSectionId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("delete-survey-section", new { instrumentSectionId }); bool deleted = await _dal.DeleteSectionAsync(instrumentSectionId, login, ct).ConfigureAwait(false); return deleted ? SuccessResponse.DeleteSuccessMessage : $"Section {instrumentSectionId} not found."; } catch (Exception ex) { GB5Trace.MarkFailed("delete-survey-section-failed", ex); _logger.LogError(ex, "DeleteSection failed for id {Id}", instrumentSectionId); throw; } } public async Task GetSectionAsync(int instrumentSectionId, LoginDTO login, CancellationToken ct) { try { return await _dal.GetSectionByIdAsync(instrumentSectionId, login, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-survey-section-failed", ex); _logger.LogError(ex, "GetSection failed for id {Id}", instrumentSectionId); throw; } } public async Task> GetSectionListAsync(int instrumentConfigId, LoginDTO login, CancellationToken ct) { try { return await _dal.GetSectionsByConfigAsync(instrumentConfigId, login, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-survey-section-list-failed", ex); _logger.LogError(ex, "GetSectionList failed for configId {Id}", instrumentConfigId); throw; } } public async Task> GetSelectListSurveySection( int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO login) { try { if (firstNumber <= 0 && maxResult <= 0) { firstNumber = -1; maxResult = -1; } return await _dal.GetSelectListSurveySection(firstNumber, maxResult, criteriaDTO, login).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-surveysection-failed", ex); _logger.LogError(ex, "GetSelectListSurveySection failed"); throw; } } public async Task SaveQuestionAsync(SaveSurveyQuestionRequest req, LoginDTO login, CancellationToken ct) { try { int questionId = req.QuestionId; // PK convention: QuestionId == 0 → insert (new AutoNumber); non-zero → update existing row. if (questionId == 0) { var autoNum = await _autoNumber.GetNumberAsync(1, "SURVEYQUESTIONBANK", login).ConfigureAwait(false); questionId = autoNum.StartNumber; await _dal.InsertQuestionBankAsync(questionId, req, login, ct).ConfigureAwait(false); } else { bool updated = await _dal.UpdateQuestionBankAsync(questionId, req, login, ct).ConfigureAwait(false); if (!updated) return $"Question {questionId} not found."; } var transAutoNum = await _autoNumber.GetNumberAsync(1, "SURVEYQUESTIONTRANS", login).ConfigureAwait(false); await _dal.UpsertQuestionTransAsync(questionId, transAutoNum.StartNumber, req.LangCode, req.QuestionText, login, ct).ConfigureAwait(false); var keepOptionIds = new List(); foreach (var option in req.Options) { int optionId = option.OptionId; // Mockup's Add/Edit Question screen never collects an option code — only option // text with auto-numbering — so synthesize one when the caller didn't supply it. var optionToSave = option; if (string.IsNullOrWhiteSpace(optionToSave.OptionCode)) optionToSave = optionToSave with { OptionCode = $"OPT{optionToSave.DisplayOrder}" }; if (optionId == 0) { var autoNum = await _autoNumber.GetNumberAsync(1, "SURVEYOPTION", login).ConfigureAwait(false); optionId = autoNum.StartNumber; await _dal.InsertOptionAsync(optionId, questionId, optionToSave, login, ct).ConfigureAwait(false); } else { await _dal.UpdateOptionAsync(optionId, optionToSave, login, ct).ConfigureAwait(false); } keepOptionIds.Add(optionId); var optTransAutoNum = await _autoNumber.GetNumberAsync(1, "SURVEYOPTIONTRANS", login).ConfigureAwait(false); await _dal.UpsertOptionTransAsync(optionId, optTransAutoNum.StartNumber, req.LangCode, option.OptionText, login, ct).ConfigureAwait(false); } await _dal.DeactivateOptionsNotInAsync(questionId, keepOptionIds, login, ct).ConfigureAwait(false); return $"{SuccessResponse.SaveSuccessMessage} {questionId}"; } catch (Exception ex) { GB5Trace.MarkFailed("save-survey-question-failed", ex); _logger.LogError(ex, "SaveQuestion failed for questionId {Id}", req.QuestionId); throw; } } public async Task> GetQuestionListAsync( string searchText, string langCode, int firstNumber, int maxResult, LoginDTO login, CancellationToken ct) { try { if (firstNumber <= 0) firstNumber = 1; if (maxResult <= 0) maxResult = 50; return await _dal.GetQuestionBankListAsync(searchText, langCode, firstNumber, maxResult, login, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-survey-question-list-failed", ex); _logger.LogError(ex, "GetQuestionList failed"); throw; } } public async Task GetQuestionAsync(int questionId, string langCode, LoginDTO login, CancellationToken ct) { try { var (question, options) = await _dal.GetQuestionByIdAsync(questionId, langCode, login, ct).ConfigureAwait(false); if (question is null) return null; return new SurveyQuestionDetailDto( question.QuestionId, question.QuestionCode, question.QuestionText, question.QuestionTypeId, question.IsRequired, question.SortOrder, question.RatingMin, question.RatingMax, question.RatingLabelLow, question.RatingLabelHigh, question.TextMaxLength, question.TextMinLength, question.YesLabel, question.NoLabel, options); } catch (Exception ex) { GB5Trace.MarkFailed("get-survey-question-failed", ex); _logger.LogError(ex, "GetQuestion failed for questionId {Id}", questionId); throw; } } public async Task> GetSelectListSurveyQuestion( int firstNumber, int maxResult, string langCode, CriteriaDTO criteriaDTO, LoginDTO login) { try { if (firstNumber <= 0 && maxResult <= 0) { firstNumber = -1; maxResult = -1; } return await _dal.GetSelectListSurveyQuestion(firstNumber, maxResult, langCode, criteriaDTO, login).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-surveyquestion-failed", ex); _logger.LogError(ex, "GetSelectListSurveyQuestion failed"); throw; } } public async Task> GetSelectListSurveyInstrument( int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO login) { try { if (firstNumber <= 0 && maxResult <= 0) { firstNumber = -1; maxResult = -1; } return await _dal.GetSelectListSurveyInstrument(firstNumber, maxResult, criteriaDTO, login).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-surveyinstrument-failed", ex); _logger.LogError(ex, "GetSelectListSurveyInstrument failed"); throw; } } public async Task DeleteQuestionAsync(int questionId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("delete-survey-question", new { questionId }); bool deleted = await _dal.DeactivateQuestionBankAsync(questionId, login, ct).ConfigureAwait(false); return deleted ? SuccessResponse.DeleteSuccessMessage : $"Question {questionId} not found."; } catch (Exception ex) { GB5Trace.MarkFailed("delete-survey-question-failed", ex); _logger.LogError(ex, "DeleteQuestion failed for id {Id}", questionId); throw; } } private async Task BuildInstrumentDtoAsync( int instrumentConfigId, string langCode, LoginDTO login, CancellationToken ct) { var (config, sections, questions, options) = await _dal.GetInstrumentAsync(instrumentConfigId, langCode, login, ct).ConfigureAwait(false); if (config is null) return new SurveyInstrumentDto(0, 0, null, null, string.Empty, 0, null, null, 0, 0, 0, 0, 0, string.Empty, new List()); return AssembleDto(config, sections, questions, options); } private static SurveyInstrumentDto AssembleDto( SurveyInstrumentConfigRow config, List sections, List questions, List options) { var optDict = options .GroupBy(o => o.InstQuestionId) .ToDictionary(g => g.Key, g => g.ToList()); var sectionDtos = sections.Select(s => { var sectionQuestions = questions.Where(q => q.InstrumentSectionId == s.InstrumentSectionId).ToList(); var questionDtos = sectionQuestions.Select(q => { optDict.TryGetValue(q.InstQuestionId, out var opts); // ISREQUIREDOVERRIDE: 0=No, 1=Yes, 2=UseDefault (from question bank IsRequired) int isRequired = q.IsRequiredOverride switch { 1 => 1, 0 => 0, _ => q.IsRequired }; var optionDtos = (opts ?? new List()).Select(o => new SurveyOptionDto( o.InstOptionId, o.OptionCode, o.OptionText, o.OptionValue, o.DisplayOrder, o.IsDefault)).ToList(); return new SurveyQuestionDto( q.InstQuestionId, q.QuestionId, q.QuestionTypeId, q.QuestionText ?? string.Empty, isRequired, q.DisplayOrder, q.ScaleMin, q.ScaleMax, q.ScaleStep, q.RatingLabelLow, q.RatingLabelHigh, q.TextMaxLength, q.TextMinLength, q.YesLabel, q.NoLabel, q.BranchRulesJson, optionDtos); }).ToList(); return new SurveySectionDto( s.InstrumentSectionId, s.SectionCode, s.SectionName, s.SlNo, 0, sectionQuestions.Count, questionDtos); }).ToList(); return new SurveyInstrumentDto( config.InstrumentConfigId, config.FlsRegistrationId, config.RegistrationCode, config.RegistrationName, config.InstrumentTitle, config.InstrumentTypeId, config.IntroText, config.ThankYouMessage, config.IsAnonymous, config.IsLocked, config.ShowProgressBar, config.AllowPartialSubmit, config.Status, config.LangDefault, sectionDtos); } }