_ShortDescription}, e.g. {@code V001_AddSideEffectsQuestion},
+ * zero-padded so lexicographic sort matches numeric order. Give each class a javadoc stating the
+ * business reason for the change — that, plus the class itself, is the permanent changelog.
+ *
+ * Example:
+ * {@code
+ * @Component
+ * public class V001_AddSideEffectsQuestionToTbCounselling implements FormStructureMigration {
+ *
+ * @Override
+ * public String migrationId() {
+ * return "V001";
+ * }
+ *
+ * @Override
+ * public void apply(DynamicFormReconciliationService svc) {
+ * FormVersion version = svc.latestVersion("TB_COUNSELLING")
+ * .orElseThrow(() -> new IllegalStateException(
+ * "TB_COUNSELLING not found — must run after TbCounsellingFormSeeder"));
+ * FormSection section = svc.section(version, "TB_SEC_B")
+ * .orElseThrow(() -> new IllegalStateException("Section TB_SEC_B not found"));
+ *
+ * SectionQuestionDTO question = new SectionQuestionDTO();
+ * question.setQuestionUuid("TB_B_Q7");
+ * question.setQuestionText("Side effects reported?");
+ * question.setQuestionType(QuestionType.RADIO);
+ * question.setOptions(List.of(yesOption, noOption));
+ * svc.ensureQuestion(section, question);
+ * }
+ * }
+ * }
+ */
+public interface FormStructureMigration {
+
+ /** Stable id, e.g. "V001" — the migration-log primary key and the sort key for apply order. Never change it once shipped. */
+ String migrationId();
+
+ /**
+ * Applies this migration's change(s) via the reconciliation service's ensureX methods.
+ * Should remain safe to re-run (ensureX no-ops on rows that already exist) even though the
+ * runner normally calls this only once per migrationId, per the tracking log.
+ */
+ void apply(DynamicFormReconciliationService reconciliationService);
+}
diff --git a/src/main/java/com/iemr/flw/seeder/migration/V001_1_CreateContactTracingVersion2.java b/src/main/java/com/iemr/flw/seeder/migration/V001_1_CreateContactTracingVersion2.java
new file mode 100644
index 000000000..78c7cd645
--- /dev/null
+++ b/src/main/java/com/iemr/flw/seeder/migration/V001_1_CreateContactTracingVersion2.java
@@ -0,0 +1,108 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.flw.seeder.migration;
+
+import com.iemr.flw.domain.iemr.FormVersion;
+import com.iemr.flw.dto.iemr.DynamicFormDTO;
+import com.iemr.flw.dto.iemr.FormSectionDTO;
+import com.iemr.flw.dto.iemr.OptionConditionDTO;
+import com.iemr.flw.dto.iemr.QuestionOptionDTO;
+import com.iemr.flw.dto.iemr.QuestionValidationDTO;
+import com.iemr.flw.dto.iemr.SectionQuestionDTO;
+import com.iemr.flw.service.DynamicFormDefinitionService;
+import com.iemr.flw.service.DynamicFormReconciliationService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * Clones the seeded version 1 of both contact tracing forms — Community Contact Tracing and
+ * Occupational Contact Tracing — into a new version 2, so the later structural changes to them are
+ * applied to version 2 only and version 1 stays exactly as the seeders created it:
+ * COMMUNITY_CONTACT_TRACING (V002, V003, V006, V007, V008) and OCCUPATION_CONTACT_TRACING (V002, V004,
+ * V005). Responses keep the versionId they were submitted against; new responses and the app use the
+ * latest version.
+ *
+ * Id "V001_1" sorts between V001 and V002, so this runs before the first migration that touches
+ * either form — every migration resolves a form via {@code latestVersion}, which is version 2 from
+ * here on.
+ *
+ * Uses {@link DynamicFormDefinitionService#updateForm}, which copies every section, question, option,
+ * validation and condition into a new version and marks the current one isLatest=false. All ids are
+ * cleared from the version-1 DTO first: the mapper copies them onto the new entities, and saving an
+ * entity with an existing id would move that version-1 row into version 2 instead of copying it.
+ * Conditions are re-wired by targetQuestionUuid/targetSectionUuid, which are kept.
+ */
+@Component
+@RequiredArgsConstructor
+public class V001_1_CreateContactTracingVersion2 implements FormStructureMigration {
+
+ private static final List FORM_UUIDS = List.of(
+ "COMMUNITY_CONTACT_TRACING",
+ "OCCUPATION_CONTACT_TRACING");
+
+ private final DynamicFormDefinitionService definitionService;
+
+ @Override
+ public String migrationId() {
+ return "V001_1";
+ }
+
+ @Override
+ public void apply(DynamicFormReconciliationService svc) {
+ FORM_UUIDS.forEach(formUuid -> createVersion2(svc, formUuid));
+ }
+
+ private void createVersion2(DynamicFormReconciliationService svc, String formUuid) {
+ FormVersion latest = svc.latestVersion(formUuid)
+ .orElseThrow(() -> new IllegalStateException(formUuid + " not found — must run after its seeder"));
+ if (latest.getVersionNumber() > 1) {
+ return;
+ }
+
+ Long formId = latest.getDynamicForm().getFormId();
+ DynamicFormDTO version1 = definitionService.getFormDefinition(formId);
+ clearIds(version1);
+ definitionService.updateForm(formId, version1);
+ }
+
+ private void clearIds(DynamicFormDTO form) {
+ for (FormSectionDTO section : form.getSections()) {
+ section.setSectionId(null);
+ for (SectionQuestionDTO question : section.getQuestions()) {
+ question.setQuestionId(null);
+ for (QuestionValidationDTO validation : question.getValidations()) {
+ validation.setValidationId(null);
+ }
+ for (QuestionOptionDTO option : question.getOptions()) {
+ option.setOptionId(null);
+ for (OptionConditionDTO condition : option.getConditions()) {
+ condition.setConditionId(null);
+ condition.setTargetQuestionId(null);
+ condition.setTargetSectionId(null);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/iemr/flw/seeder/migration/V001_ReplaceTfuAdrOptions.java b/src/main/java/com/iemr/flw/seeder/migration/V001_ReplaceTfuAdrOptions.java
new file mode 100644
index 000000000..6fc90ff6e
--- /dev/null
+++ b/src/main/java/com/iemr/flw/seeder/migration/V001_ReplaceTfuAdrOptions.java
@@ -0,0 +1,104 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.flw.seeder.migration;
+
+import com.iemr.flw.domain.iemr.FormSection;
+import com.iemr.flw.domain.iemr.FormVersion;
+import com.iemr.flw.domain.iemr.SectionQuestion;
+import com.iemr.flw.dto.iemr.QuestionOptionDTO;
+import com.iemr.flw.service.DynamicFormReconciliationService;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * Replaces the TPT Follow Up form's "Adverse Drug Reactions" (TFU_ADR) option list with the
+ * clinically revised set of symptoms, per the updated ADR checklist supplied for this form.
+ *
+ * 7 of the 24 existing options already match the new list exactly (same wording) and are left
+ * untouched by {@code ensureOption}'s natural-key match: NAUSEA, VOMITING, ABDOMINAL_PAIN,
+ * DARK_COLOUR_URINE, ITCHING, TINGLING_BURNING_NUMBNESS_HANDS_FEET, FLU_LIKE_SYNDROME. The other
+ * 17 old options are no longer part of the revised list and are soft-removed (never hard-deleted —
+ * historical responses may reference them); the 13 new options are added.
+ */
+@Component
+public class V001_ReplaceTfuAdrOptions implements FormStructureMigration {
+
+ private static final List REMOVED_OPTION_VALUES = List.of(
+ "UNKNOWN",
+ "FLU_LIKE_SYNDROME_WITH_DIZZINESS_HEADACHE",
+ "YELLOWISH_DISCOLORATION_SKIN_EYES",
+ "DISCOLORATION_BODY_FLUIDS",
+ "PALE_STOOL",
+ "SKIN_RASH",
+ "SLEEPINESS_LETHARGY",
+ "PERSISTENT_UNFORMED_WATERY_STOOLS",
+ "MENTAL_CHANGES_SIGNS_BLEEDING",
+ "CONVULSIONS",
+ "ANAEMIA",
+ "ARTHRALGIA",
+ "DECREASED_APPETITE",
+ "HYPOTENSION_SYNCOPE",
+ "CONJUCTIVITIES",
+ "SHOCK",
+ "NONE_REPORTED");
+
+ @Override
+ public String migrationId() {
+ return "V001";
+ }
+
+ @Override
+ public void apply(DynamicFormReconciliationService svc) {
+ FormVersion version = svc.latestVersion("TPT_FOLLOW_UP")
+ .orElseThrow(() -> new IllegalStateException(
+ "TPT_FOLLOW_UP not found — must run after TptFollowUpFormSeeder"));
+ FormSection section = svc.section(version, "TFU_SEC_2")
+ .orElseThrow(() -> new IllegalStateException("Section TFU_SEC_2 not found"));
+ SectionQuestion question = svc.question(section, "TFU_ADR")
+ .orElseThrow(() -> new IllegalStateException("Question TFU_ADR not found"));
+
+ REMOVED_OPTION_VALUES.forEach(value -> svc.option(question, value)
+ .ifPresent(opt -> svc.removeOption(opt.getOptionId())));
+
+ addOption(svc, question, "Yellowness of Skin", "YELLOWNESS_OF_SKIN");
+ addOption(svc, question, "Loose motions >4 times in a day", "LOOSE_MOTIONS_GT4_PER_DAY");
+ addOption(svc, question, "Rashes", "RASHES");
+ addOption(svc, question, "Pain in Joints", "PAIN_IN_JOINTS");
+ addOption(svc, question, "Impaired vision: Pain, Blurring of vision, Disturbance in colour vision", "IMPAIRED_VISION");
+ addOption(svc, question, "Swelling of face or legs, Less or no urine", "SWELLING_FACE_LEGS_LESS_NO_URINE");
+ addOption(svc, question, "Seeing abnormal things, changes of thoughts, suicidal thoughts", "ABNORMAL_THOUGHTS_SUICIDAL");
+ addOption(svc, question, "Tiredness, lethargy, headache, giddiness, pale look, palpitations", "TIREDNESS_LETHARGY_GIDDINESS_PALPITATIONS");
+ addOption(svc, question, "Ringing in ears, Loss of hearing, dizziness and loss of balance leading to recurrent fall", "TINNITUS_HEARING_LOSS_DIZZINESS_FALL");
+ addOption(svc, question, "Slowness of activities, Swelling of face or neck, dis-appropriate weight gain", "SLOWNESS_FACIAL_NECK_SWELLING_WEIGHT_GAIN");
+ addOption(svc, question, "Pain and swelling in muscles and Tendons, difficulty in movement", "MUSCLE_TENDON_PAIN_SWELLING");
+ addOption(svc, question, "Convulsion", "CONVULSION");
+ addOption(svc, question, "Orange and red color of urine, sweat, sputum, saliva or tears", "ORANGE_RED_DISCOLORATION_BODY_FLUIDS");
+ }
+
+ private void addOption(DynamicFormReconciliationService svc, SectionQuestion question, String label, String value) {
+ QuestionOptionDTO dto = new QuestionOptionDTO();
+ dto.setOptionLabel(label);
+ dto.setOptionValue(value);
+ svc.ensureOption(question, dto);
+ }
+}
diff --git a/src/main/java/com/iemr/flw/seeder/migration/V002_RenameNoOfContactsQuestion.java b/src/main/java/com/iemr/flw/seeder/migration/V002_RenameNoOfContactsQuestion.java
new file mode 100644
index 000000000..836c1b1e3
--- /dev/null
+++ b/src/main/java/com/iemr/flw/seeder/migration/V002_RenameNoOfContactsQuestion.java
@@ -0,0 +1,83 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.flw.seeder.migration;
+
+import com.iemr.flw.domain.iemr.FormSection;
+import com.iemr.flw.domain.iemr.FormVersion;
+import com.iemr.flw.domain.iemr.SectionQuestion;
+import com.iemr.flw.dto.iemr.SectionQuestionDTO;
+import com.iemr.flw.service.DynamicFormReconciliationService;
+import org.springframework.stereotype.Component;
+
+/**
+ * Renames the "No. of contacts" question to "Total No. of contacts" (and its Hindi translation)
+ * in both the Community Contact Tracing (CCT_NO_OF_CONTACTS) and Occupational Contact Tracing
+ * (OCT_NO_OF_CONTACTS) forms, per the revised field-label wording supplied for these forms.
+ *
+ * Only the label text changes — type, mandatoriness and position are read off the existing row
+ * and passed back unchanged, since {@code updateQuestion} overwrites all of its scalar fields
+ * unconditionally.
+ */
+@Component
+public class V002_RenameNoOfContactsQuestion implements FormStructureMigration {
+
+ private static final String NEW_TEXT = "Total No. of contacts";
+ private static final String NEW_TEXT_HINDI = "कुल संपर्कों की संख्या";
+
+ @Override
+ public String migrationId() {
+ return "V002";
+ }
+
+ @Override
+ public void apply(DynamicFormReconciliationService svc) {
+ renameQuestion(svc, "COMMUNITY_CONTACT_TRACING", "CCT_SEC_1", "CCT_NO_OF_CONTACTS");
+ renameQuestion(svc, "OCCUPATION_CONTACT_TRACING", "OCT_SEC_1", "OCT_NO_OF_CONTACTS");
+ }
+
+ private void renameQuestion(DynamicFormReconciliationService svc, String formUuid, String sectionUuid,
+ String questionUuid) {
+ FormVersion version = svc.latestVersion(formUuid)
+ .orElseThrow(() -> new IllegalStateException(
+ formUuid + " not found — must run after its seeder"));
+ FormSection section = svc.section(version, sectionUuid)
+ .orElseThrow(() -> new IllegalStateException("Section " + sectionUuid + " not found"));
+ SectionQuestion question = svc.question(section, questionUuid)
+ .orElseThrow(() -> new IllegalStateException("Question " + questionUuid + " not found"));
+
+ if (NEW_TEXT.equals(question.getQuestionText()) && NEW_TEXT_HINDI.equals(question.getQuestionTextHindi())) {
+ return;
+ }
+
+ SectionQuestionDTO dto = new SectionQuestionDTO();
+ dto.setQuestionText(NEW_TEXT);
+ dto.setQuestionTextHindi(NEW_TEXT_HINDI);
+ dto.setQuestionType(question.getQuestionType());
+ dto.setIsMandatory(question.getIsMandatory());
+ dto.setMaxLength(question.getMaxLength());
+ dto.setDefaultValue(question.getDefaultValue());
+ dto.setContainsPii(question.getContainsPii());
+ dto.setDisplayOrder(question.getDisplayOrder());
+
+ svc.updateQuestion(question.getQuestionId(), dto);
+ }
+}
diff --git a/src/main/java/com/iemr/flw/seeder/migration/V003_ChangeExposureSettingToDropdownMulti.java b/src/main/java/com/iemr/flw/seeder/migration/V003_ChangeExposureSettingToDropdownMulti.java
new file mode 100644
index 000000000..ba69d37b4
--- /dev/null
+++ b/src/main/java/com/iemr/flw/seeder/migration/V003_ChangeExposureSettingToDropdownMulti.java
@@ -0,0 +1,76 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.flw.seeder.migration;
+
+import com.iemr.flw.domain.iemr.FormSection;
+import com.iemr.flw.domain.iemr.FormVersion;
+import com.iemr.flw.domain.iemr.SectionQuestion;
+import com.iemr.flw.dto.iemr.SectionQuestionDTO;
+import com.iemr.flw.masterEnum.QuestionType;
+import com.iemr.flw.service.DynamicFormReconciliationService;
+import org.springframework.stereotype.Component;
+
+/**
+ * Changes the Community Contact Tracing form's "Place of exposure" question (CCT_EXPOSURE_SETTING)
+ * from CHECKBOX_MULTI to DROPDOWN_MULTI, so the 12 place options render as a multi-select dropdown
+ * instead of a long checkbox list.
+ *
+ * Both types submit {@code optionValues} and store one response row per selected option, so
+ * existing responses remain valid. Options and the OTHER_PLACE → CCT_EXPOSURE_SETTING_OTHER
+ * conditional rule are untouched. All other scalar fields are read off the existing row and passed
+ * back unchanged, since {@code updateQuestion} overwrites all of them unconditionally.
+ */
+@Component
+public class V003_ChangeExposureSettingToDropdownMulti implements FormStructureMigration {
+
+ @Override
+ public String migrationId() {
+ return "V003";
+ }
+
+ @Override
+ public void apply(DynamicFormReconciliationService svc) {
+ FormVersion version = svc.latestVersion("COMMUNITY_CONTACT_TRACING")
+ .orElseThrow(() -> new IllegalStateException(
+ "COMMUNITY_CONTACT_TRACING not found — must run after CommunityContactTracingFormSeeder"));
+ FormSection section = svc.section(version, "CCT_SEC_1")
+ .orElseThrow(() -> new IllegalStateException("Section CCT_SEC_1 not found"));
+ SectionQuestion question = svc.question(section, "CCT_EXPOSURE_SETTING")
+ .orElseThrow(() -> new IllegalStateException("Question CCT_EXPOSURE_SETTING not found"));
+
+ if (question.getQuestionType() == QuestionType.DROPDOWN_MULTI) {
+ return;
+ }
+
+ SectionQuestionDTO dto = new SectionQuestionDTO();
+ dto.setQuestionText(question.getQuestionText());
+ dto.setQuestionTextHindi(question.getQuestionTextHindi());
+ dto.setQuestionType(QuestionType.DROPDOWN_MULTI);
+ dto.setIsMandatory(question.getIsMandatory());
+ dto.setMaxLength(question.getMaxLength());
+ dto.setDefaultValue(question.getDefaultValue());
+ dto.setContainsPii(question.getContainsPii());
+ dto.setDisplayOrder(question.getDisplayOrder());
+
+ svc.updateQuestion(question.getQuestionId(), dto);
+ }
+}
diff --git a/src/main/java/com/iemr/flw/seeder/migration/V004_ChangeOccupationToDropdown.java b/src/main/java/com/iemr/flw/seeder/migration/V004_ChangeOccupationToDropdown.java
new file mode 100644
index 000000000..24db1f454
--- /dev/null
+++ b/src/main/java/com/iemr/flw/seeder/migration/V004_ChangeOccupationToDropdown.java
@@ -0,0 +1,76 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.flw.seeder.migration;
+
+import com.iemr.flw.domain.iemr.FormSection;
+import com.iemr.flw.domain.iemr.FormVersion;
+import com.iemr.flw.domain.iemr.SectionQuestion;
+import com.iemr.flw.dto.iemr.SectionQuestionDTO;
+import com.iemr.flw.masterEnum.QuestionType;
+import com.iemr.flw.service.DynamicFormReconciliationService;
+import org.springframework.stereotype.Component;
+
+/**
+ * Changes the Occupational Contact Tracing form's "Occupation" question (OCT_OCCUPATION) from RADIO
+ * to DROPDOWN, so the 11 occupation options render as a single-select dropdown instead of a long
+ * radio list.
+ *
+ * Both types submit a single {@code optionValue} and store one response row, so existing responses
+ * remain valid. Options and their conditional rules (address questions, OTHER_OCCUPATION →
+ * OCT_OCCUPATION_OTHER) are untouched. All other scalar fields are read off the existing row and
+ * passed back unchanged, since {@code updateQuestion} overwrites all of them unconditionally.
+ */
+@Component
+public class V004_ChangeOccupationToDropdown implements FormStructureMigration {
+
+ @Override
+ public String migrationId() {
+ return "V004";
+ }
+
+ @Override
+ public void apply(DynamicFormReconciliationService svc) {
+ FormVersion version = svc.latestVersion("OCCUPATION_CONTACT_TRACING")
+ .orElseThrow(() -> new IllegalStateException(
+ "OCCUPATION_CONTACT_TRACING not found — must run after OccupationalContactTracingFormSeeder"));
+ FormSection section = svc.section(version, "OCT_SEC_1")
+ .orElseThrow(() -> new IllegalStateException("Section OCT_SEC_1 not found"));
+ SectionQuestion question = svc.question(section, "OCT_OCCUPATION")
+ .orElseThrow(() -> new IllegalStateException("Question OCT_OCCUPATION not found"));
+
+ if (question.getQuestionType() == QuestionType.DROPDOWN) {
+ return;
+ }
+
+ SectionQuestionDTO dto = new SectionQuestionDTO();
+ dto.setQuestionText(question.getQuestionText());
+ dto.setQuestionTextHindi(question.getQuestionTextHindi());
+ dto.setQuestionType(QuestionType.DROPDOWN);
+ dto.setIsMandatory(question.getIsMandatory());
+ dto.setMaxLength(question.getMaxLength());
+ dto.setDefaultValue(question.getDefaultValue());
+ dto.setContainsPii(question.getContainsPii());
+ dto.setDisplayOrder(question.getDisplayOrder());
+
+ svc.updateQuestion(question.getQuestionId(), dto);
+ }
+}
diff --git a/src/main/java/com/iemr/flw/seeder/migration/V005_AddAreaOfSharedSpaceQuestion.java b/src/main/java/com/iemr/flw/seeder/migration/V005_AddAreaOfSharedSpaceQuestion.java
new file mode 100644
index 000000000..11a6a3d0c
--- /dev/null
+++ b/src/main/java/com/iemr/flw/seeder/migration/V005_AddAreaOfSharedSpaceQuestion.java
@@ -0,0 +1,115 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.flw.seeder.migration;
+
+import com.iemr.flw.domain.iemr.FormSection;
+import com.iemr.flw.domain.iemr.FormVersion;
+import com.iemr.flw.domain.iemr.QuestionOption;
+import com.iemr.flw.domain.iemr.SectionQuestion;
+import com.iemr.flw.dto.iemr.OptionConditionDTO;
+import com.iemr.flw.dto.iemr.QuestionValidationDTO;
+import com.iemr.flw.dto.iemr.SectionQuestionDTO;
+import com.iemr.flw.masterEnum.QuestionType;
+import com.iemr.flw.masterEnum.ValidationType;
+import com.iemr.flw.service.DynamicFormReconciliationService;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * Adds an "Approximate Area of Shared Space" question to the Occupational Contact Tracing form,
+ * directly after "Type of Space" (OCT_TYPE_OF_SPACE), per the revised criteria for this form:
+ * enabled once a Type of Space is selected, whole square metres only, accepted range 50–5000.
+ *
+ * Mapped to Type of Space as a condition: hidden by default, and each of the 3 OCT_TYPE_OF_SPACE
+ * options reveals it (SHOW_QUESTION) and makes it mandatory (MANDATORY_IF) — conditions attach to
+ * options, so "any Type of Space selected" is one condition per option, all targeting this single
+ * question. Conventions used, since questions carry no dedicated fields for these:
+ * the helper text is stored in {@code defaultValue}, and the unit as a {@link ValidationType#UNIT}
+ * validation whose {@code validationParam} is the unit label.
+ *
+ * Inserted at display order 7 — {@code ensureQuestion} shifts "Daily duration of contact" to 8 and
+ * "Remarks" to 9.
+ */
+@Component
+public class V005_AddAreaOfSharedSpaceQuestion implements FormStructureMigration {
+
+ private static final String QUESTION_UUID = "OCT_AREA_OF_SHARED_SPACE";
+
+ private static final List TYPE_OF_SPACE_VALUES = List.of(
+ "OPEN_SPACE",
+ "CLOSED_SPACE_VENTILATED",
+ "CLOSED_SPACE_NOT_VENTILATED");
+
+ @Override
+ public String migrationId() {
+ return "V005";
+ }
+
+ @Override
+ public void apply(DynamicFormReconciliationService svc) {
+ FormVersion version = svc.latestVersion("OCCUPATION_CONTACT_TRACING")
+ .orElseThrow(() -> new IllegalStateException(
+ "OCCUPATION_CONTACT_TRACING not found — must run after OccupationalContactTracingFormSeeder"));
+ FormSection section = svc.section(version, "OCT_SEC_1")
+ .orElseThrow(() -> new IllegalStateException("Section OCT_SEC_1 not found"));
+ SectionQuestion typeOfSpace = svc.question(section, "OCT_TYPE_OF_SPACE")
+ .orElseThrow(() -> new IllegalStateException("Question OCT_TYPE_OF_SPACE not found"));
+
+ SectionQuestionDTO dto = new SectionQuestionDTO();
+ dto.setQuestionUuid(QUESTION_UUID);
+ dto.setQuestionText("Approximate Area of Shared Space");
+ dto.setQuestionTextHindi("साझा स्थान का अनुमानित क्षेत्रफल");
+ dto.setQuestionType(QuestionType.NUMBER_PICKER);
+ dto.setDefaultValue("Give estimated measurement of area");
+ dto.setIsMandatory(true);
+ dto.setDisplayOrder(7);
+ dto.setVisibleByDefault(false);
+ dto.setOptions(List.of());
+ dto.setValidations(List.of());
+ SectionQuestion area = svc.ensureQuestion(section, dto);
+
+ svc.ensureValidation(area, validation(ValidationType.REGEX,
+ "^([5-9][0-9]|[1-9][0-9]{2}|[1-4][0-9]{3}|5000)$", "Enter a value between 50 and 5000."));
+ svc.ensureValidation(area, validation(ValidationType.UNIT, "sq. m.", "Value is in sq. m."));
+
+ for (String value : TYPE_OF_SPACE_VALUES) {
+ svc.ensureValidation(area, validation(ValidationType.MANDATORY_IF,
+ "OCT_TYPE_OF_SPACE=" + value, "This field is mandatory"));
+
+ QuestionOption option = svc.option(typeOfSpace, value)
+ .orElseThrow(() -> new IllegalStateException("Option OCT_TYPE_OF_SPACE=" + value + " not found"));
+ OptionConditionDTO show = new OptionConditionDTO();
+ show.setActionType("SHOW_QUESTION");
+ show.setTargetQuestionUuid(QUESTION_UUID);
+ svc.ensureCondition(option, show, version);
+ }
+ }
+
+ private QuestionValidationDTO validation(ValidationType type, String param, String errorMessage) {
+ QuestionValidationDTO v = new QuestionValidationDTO();
+ v.setValidationType(type);
+ v.setValidationParam(param);
+ v.setErrorMessage(errorMessage);
+ return v;
+ }
+}
diff --git a/src/main/java/com/iemr/flw/seeder/migration/V006_PerRelationshipTypeOfSpace.java b/src/main/java/com/iemr/flw/seeder/migration/V006_PerRelationshipTypeOfSpace.java
new file mode 100644
index 000000000..822ea0867
--- /dev/null
+++ b/src/main/java/com/iemr/flw/seeder/migration/V006_PerRelationshipTypeOfSpace.java
@@ -0,0 +1,172 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.flw.seeder.migration;
+
+import com.iemr.flw.domain.iemr.FormSection;
+import com.iemr.flw.domain.iemr.FormVersion;
+import com.iemr.flw.domain.iemr.QuestionOption;
+import com.iemr.flw.domain.iemr.SectionQuestion;
+import com.iemr.flw.dto.iemr.OptionConditionDTO;
+import com.iemr.flw.dto.iemr.QuestionOptionDTO;
+import com.iemr.flw.dto.iemr.QuestionValidationDTO;
+import com.iemr.flw.dto.iemr.SectionQuestionDTO;
+import com.iemr.flw.masterEnum.QuestionType;
+import com.iemr.flw.masterEnum.ValidationType;
+import com.iemr.flw.service.DynamicFormReconciliationService;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * Replaces the Community Contact Tracing form's single, standalone "Type of Space" question
+ * (CCT_TYPE_OF_SPACE) with one per "Relationship with community contacts" (CCT_RELATIONSHIP)
+ * option, each followed by its own "Approximate Area of Shared Space", so the exposure setting is
+ * captured per relationship type:
+ *
+ *
+ * CCT_RELATIONSHIP=<R> ─ SHOW_QUESTION ─► CCT_TYPE_OF_SPACE_<R>
+ * └─ each option ─ SHOW_QUESTION ─► CCT_AREA_OF_SHARED_SPACE_<R>
+ *
+ *
+ * The old CCT_TYPE_OF_SPACE is soft-removed (never hard-deleted — historical responses may
+ * reference it). The 14 new questions (a Type of Space / Area pair per relationship) are inserted
+ * into its slot, all hidden by default and mandatory only when revealed (MANDATORY_IF). Area
+ * questions follow V006's conventions: helper text in {@code defaultValue}, unit as a
+ * {@link ValidationType#UNIT} validation, whole square metres 50–5000.
+ */
+@Component
+public class V006_PerRelationshipTypeOfSpace implements FormStructureMigration {
+
+ private static final List RELATIONSHIP_VALUES = List.of(
+ "NEIGHBOUR",
+ "FRIEND",
+ "FELLOW_WORSHIPPER",
+ "FELLOW_COMMUTER",
+ "COMMUNITY_GROUP_MEMBER",
+ "FELLOW_PATIENT",
+ "OTHER");
+
+ /** {label, labelHindi, value} — same options as the retired CCT_TYPE_OF_SPACE. */
+ private static final List TYPE_OF_SPACE_OPTIONS = List.of(
+ new String[] {"Open space", "खुला स्थान", "OPEN_SPACE"},
+ new String[] {"Closed space with ventilation", "वेंटिलेशन के साथ बंद स्थान", "CLOSED_SPACE_VENTILATED"},
+ new String[] {"Closed space with no ventilation", "बिना वेंटिलेशन के बंद स्थान", "CLOSED_SPACE_NOT_VENTILATED"});
+
+ private static final String AREA_REGEX = "^([5-9][0-9]|[1-9][0-9]{2}|[1-4][0-9]{3}|5000)$";
+ private static final String MANDATORY_ERROR = "This field is mandatory";
+
+ @Override
+ public String migrationId() {
+ return "V006";
+ }
+
+ @Override
+ public void apply(DynamicFormReconciliationService svc) {
+ FormVersion version = svc.latestVersion("COMMUNITY_CONTACT_TRACING")
+ .orElseThrow(() -> new IllegalStateException(
+ "COMMUNITY_CONTACT_TRACING not found — must run after CommunityContactTracingFormSeeder"));
+ FormSection section = svc.section(version, "CCT_SEC_1")
+ .orElseThrow(() -> new IllegalStateException("Section CCT_SEC_1 not found"));
+ SectionQuestion relationship = svc.question(section, "CCT_RELATIONSHIP")
+ .orElseThrow(() -> new IllegalStateException("Question CCT_RELATIONSHIP not found"));
+ SectionQuestion oldTypeOfSpace = svc.question(section, "CCT_TYPE_OF_SPACE")
+ .orElseThrow(() -> new IllegalStateException("Question CCT_TYPE_OF_SPACE not found"));
+
+ int base = oldTypeOfSpace.getDisplayOrder();
+ if (Boolean.TRUE.equals(oldTypeOfSpace.getIsActive())) {
+ svc.unlinkQuestion(oldTypeOfSpace.getQuestionId());
+ }
+
+ for (int i = 0; i < RELATIONSHIP_VALUES.size(); i++) {
+ String r = RELATIONSHIP_VALUES.get(i);
+ String typeOfSpaceUuid = "CCT_TYPE_OF_SPACE_" + r;
+ String areaUuid = "CCT_AREA_OF_SHARED_SPACE_" + r;
+
+ // Type of Space for this relationship — revealed by CCT_RELATIONSHIP=
+ SectionQuestion typeOfSpace = svc.ensureQuestion(section, question(typeOfSpaceUuid,
+ "Type of Space", "स्थान का प्रकार", QuestionType.RADIO, null, base + 1 + 2 * i));
+ svc.ensureValidation(typeOfSpace, validation(ValidationType.MANDATORY_IF,
+ "CCT_RELATIONSHIP=" + r, MANDATORY_ERROR));
+ QuestionOption relationshipOption = svc.option(relationship, r)
+ .orElseThrow(() -> new IllegalStateException("Option CCT_RELATIONSHIP=" + r + " not found"));
+ svc.ensureCondition(relationshipOption, showQuestion(typeOfSpaceUuid), version);
+
+ // Approximate Area of Shared Space for this relationship — revealed by any of its Type of Space options
+ SectionQuestion area = svc.ensureQuestion(section, question(areaUuid,
+ "Approximate Area of Shared Space", "साझा स्थान का अनुमानित क्षेत्रफल", QuestionType.NUMBER_PICKER,
+ "Give estimated measurement of area", base + 2 + 2 * i));
+ svc.ensureValidation(area, validation(ValidationType.REGEX, AREA_REGEX, "Enter a value between 50 and 5000."));
+ svc.ensureValidation(area, validation(ValidationType.UNIT, "sq. m.", "Value is in sq. m."));
+
+ for (int j = 0; j < TYPE_OF_SPACE_OPTIONS.size(); j++) {
+ String[] o = TYPE_OF_SPACE_OPTIONS.get(j);
+ QuestionOption spaceOption = svc.ensureOption(typeOfSpace, option(o[0], o[1], o[2], j + 1));
+ svc.ensureValidation(area, validation(ValidationType.MANDATORY_IF,
+ typeOfSpaceUuid + "=" + o[2], MANDATORY_ERROR));
+ svc.ensureCondition(spaceOption, showQuestion(areaUuid), version);
+ }
+ }
+
+ }
+
+ private SectionQuestionDTO question(String uuid, String text, String textHindi, QuestionType type,
+ String defaultValue, int order) {
+ SectionQuestionDTO q = new SectionQuestionDTO();
+ q.setQuestionUuid(uuid);
+ q.setQuestionText(text);
+ q.setQuestionTextHindi(textHindi);
+ q.setQuestionType(type);
+ q.setDefaultValue(defaultValue);
+ q.setIsMandatory(true);
+ q.setDisplayOrder(order);
+ q.setVisibleByDefault(false);
+ q.setOptions(List.of());
+ q.setValidations(List.of());
+ return q;
+ }
+
+ private QuestionOptionDTO option(String label, String labelHindi, String value, int order) {
+ QuestionOptionDTO o = new QuestionOptionDTO();
+ o.setOptionLabel(label);
+ o.setOptionLabelHindi(labelHindi);
+ o.setOptionValue(value);
+ o.setOptionValueHindi(labelHindi);
+ o.setDisplayOrder(order);
+ o.setConditions(List.of());
+ return o;
+ }
+
+ private QuestionValidationDTO validation(ValidationType type, String param, String errorMessage) {
+ QuestionValidationDTO v = new QuestionValidationDTO();
+ v.setValidationType(type);
+ v.setValidationParam(param);
+ v.setErrorMessage(errorMessage);
+ return v;
+ }
+
+ private OptionConditionDTO showQuestion(String targetQuestionUuid) {
+ OptionConditionDTO c = new OptionConditionDTO();
+ c.setActionType("SHOW_QUESTION");
+ c.setTargetQuestionUuid(targetQuestionUuid);
+ return c;
+ }
+}
diff --git a/src/main/java/com/iemr/flw/seeder/migration/V007_AddRelationshipHoursQuestions.java b/src/main/java/com/iemr/flw/seeder/migration/V007_AddRelationshipHoursQuestions.java
new file mode 100644
index 000000000..3486eea07
--- /dev/null
+++ b/src/main/java/com/iemr/flw/seeder/migration/V007_AddRelationshipHoursQuestions.java
@@ -0,0 +1,129 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.flw.seeder.migration;
+
+import com.iemr.flw.domain.iemr.FormSection;
+import com.iemr.flw.domain.iemr.FormVersion;
+import com.iemr.flw.domain.iemr.QuestionOption;
+import com.iemr.flw.domain.iemr.SectionQuestion;
+import com.iemr.flw.dto.iemr.OptionConditionDTO;
+import com.iemr.flw.dto.iemr.QuestionValidationDTO;
+import com.iemr.flw.dto.iemr.SectionQuestionDTO;
+import com.iemr.flw.masterEnum.QuestionType;
+import com.iemr.flw.masterEnum.ValidationType;
+import com.iemr.flw.service.DynamicFormReconciliationService;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * Adds a mandatory "Time spent with contact in hours" number picker to the Community Contact
+ * Tracing form for each "Relationship with community contacts" (CCT_RELATIONSHIP) option, so the
+ * exposure duration is captured per relationship type alongside its existing contact count.
+ *
+ * One question per option (CCT_RELATIONSHIP_HOURS_<OPTION_VALUE>) rather than a shared one,
+ * since CCT_RELATIONSHIP is multi-select and a shared field couldn't disambiguate which duration
+ * belongs to which relationship. Each is hidden by default, revealed by a SHOW_QUESTION condition
+ * on its option, and mandatory only when that option is selected (MANDATORY_IF).
+ *
+ * Each question is inserted directly after its relationship's "Approximate Area of Shared Space"
+ * (CCT_AREA_OF_SHARED_SPACE_<OPTION_VALUE>), so every relationship's revealed questions read
+ * Type of Space, Area, Time spent; {@code ensureQuestion} shifts everything after it down by one.
+ * Must run after V006_PerRelationshipTypeOfSpace, which creates those Area questions — and since it
+ * runs later, each option's SHOW_QUESTION conditions are also created in that same order.
+ */
+@Component
+public class V007_AddRelationshipHoursQuestions implements FormStructureMigration {
+
+ private static final List RELATIONSHIP_VALUES = List.of(
+ "NEIGHBOUR",
+ "FRIEND",
+ "FELLOW_WORSHIPPER",
+ "FELLOW_COMMUTER",
+ "COMMUNITY_GROUP_MEMBER",
+ "FELLOW_PATIENT",
+ "OTHER");
+
+ private static final String QUESTION_TEXT = "Time spent with contact in hours";
+ private static final String QUESTION_TEXT_HINDI = "संपर्क के साथ बिताया गया समय (घंटों में)";
+ private static final String HOURS_REGEX = "^([0-9]|1[0-9]|2[0-4])$";
+ private static final String HOURS_ERROR = "Enter whole hours between 0 and 24";
+
+ @Override
+ public String migrationId() {
+ return "V007";
+ }
+
+ @Override
+ public void apply(DynamicFormReconciliationService svc) {
+ FormVersion version = svc.latestVersion("COMMUNITY_CONTACT_TRACING")
+ .orElseThrow(() -> new IllegalStateException(
+ "COMMUNITY_CONTACT_TRACING not found — must run after CommunityContactTracingFormSeeder"));
+ FormSection section = svc.section(version, "CCT_SEC_1")
+ .orElseThrow(() -> new IllegalStateException("Section CCT_SEC_1 not found"));
+ SectionQuestion relationship = svc.question(section, "CCT_RELATIONSHIP")
+ .orElseThrow(() -> new IllegalStateException("Question CCT_RELATIONSHIP not found"));
+
+ for (String value : RELATIONSHIP_VALUES) {
+ String questionUuid = "CCT_RELATIONSHIP_HOURS_" + value;
+ String areaUuid = "CCT_AREA_OF_SHARED_SPACE_" + value;
+
+ SectionQuestion area = svc.question(section, areaUuid)
+ .orElseThrow(() -> new IllegalStateException(
+ "Question " + areaUuid + " not found — must run after V006_PerRelationshipTypeOfSpace"));
+ SectionQuestion hours = svc.ensureQuestion(section,
+ hoursQuestion(questionUuid, area.getDisplayOrder() + 1));
+ svc.ensureValidation(hours, validation(ValidationType.REGEX, HOURS_REGEX, HOURS_ERROR));
+ svc.ensureValidation(hours, validation(ValidationType.MANDATORY_IF,
+ "CCT_RELATIONSHIP=" + value, "This field is mandatory"));
+
+ QuestionOption option = svc.option(relationship, value)
+ .orElseThrow(() -> new IllegalStateException("Option CCT_RELATIONSHIP=" + value + " not found"));
+ OptionConditionDTO show = new OptionConditionDTO();
+ show.setActionType("SHOW_QUESTION");
+ show.setTargetQuestionUuid(questionUuid);
+ svc.ensureCondition(option, show, version);
+ }
+ }
+
+ private SectionQuestionDTO hoursQuestion(String uuid, int order) {
+ SectionQuestionDTO q = new SectionQuestionDTO();
+ q.setQuestionUuid(uuid);
+ q.setQuestionText(QUESTION_TEXT);
+ q.setQuestionTextHindi(QUESTION_TEXT_HINDI);
+ q.setQuestionType(QuestionType.NUMBER_PICKER);
+ q.setIsMandatory(true);
+ q.setDisplayOrder(order);
+ q.setVisibleByDefault(false);
+ q.setOptions(List.of());
+ q.setValidations(List.of());
+ return q;
+ }
+
+ private QuestionValidationDTO validation(ValidationType type, String param, String errorMessage) {
+ QuestionValidationDTO v = new QuestionValidationDTO();
+ v.setValidationType(type);
+ v.setValidationParam(param);
+ v.setErrorMessage(errorMessage);
+ return v;
+ }
+}
diff --git a/src/main/java/com/iemr/flw/seeder/migration/V008_ChangeRelationshipToDropdownMulti.java b/src/main/java/com/iemr/flw/seeder/migration/V008_ChangeRelationshipToDropdownMulti.java
new file mode 100644
index 000000000..7db08a382
--- /dev/null
+++ b/src/main/java/com/iemr/flw/seeder/migration/V008_ChangeRelationshipToDropdownMulti.java
@@ -0,0 +1,77 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.flw.seeder.migration;
+
+import com.iemr.flw.domain.iemr.FormSection;
+import com.iemr.flw.domain.iemr.FormVersion;
+import com.iemr.flw.domain.iemr.SectionQuestion;
+import com.iemr.flw.dto.iemr.SectionQuestionDTO;
+import com.iemr.flw.masterEnum.QuestionType;
+import com.iemr.flw.service.DynamicFormReconciliationService;
+import org.springframework.stereotype.Component;
+
+/**
+ * Changes the Community Contact Tracing form's "Relationship with community contacts" question
+ * (CCT_RELATIONSHIP) from CHECKBOX_MULTI to DROPDOWN_MULTI, so the 7 relationship options render as
+ * a multi-select dropdown instead of a checkbox list.
+ *
+ * Both types submit {@code optionValues} and store one response row per selected option, so
+ * existing responses remain valid. Options and their conditions (per-relationship count, Type of
+ * Space and hours questions, and "Other" text) are untouched. All other scalar fields are read off
+ * the existing row and passed back unchanged, since {@code updateQuestion} overwrites all of them
+ * unconditionally.
+ */
+@Component
+public class V008_ChangeRelationshipToDropdownMulti implements FormStructureMigration {
+
+ @Override
+ public String migrationId() {
+ return "V008";
+ }
+
+ @Override
+ public void apply(DynamicFormReconciliationService svc) {
+ FormVersion version = svc.latestVersion("COMMUNITY_CONTACT_TRACING")
+ .orElseThrow(() -> new IllegalStateException(
+ "COMMUNITY_CONTACT_TRACING not found — must run after CommunityContactTracingFormSeeder"));
+ FormSection section = svc.section(version, "CCT_SEC_1")
+ .orElseThrow(() -> new IllegalStateException("Section CCT_SEC_1 not found"));
+ SectionQuestion question = svc.question(section, "CCT_RELATIONSHIP")
+ .orElseThrow(() -> new IllegalStateException("Question CCT_RELATIONSHIP not found"));
+
+ if (question.getQuestionType() == QuestionType.DROPDOWN_MULTI) {
+ return;
+ }
+
+ SectionQuestionDTO dto = new SectionQuestionDTO();
+ dto.setQuestionText(question.getQuestionText());
+ dto.setQuestionTextHindi(question.getQuestionTextHindi());
+ dto.setQuestionType(QuestionType.DROPDOWN_MULTI);
+ dto.setIsMandatory(question.getIsMandatory());
+ dto.setMaxLength(question.getMaxLength());
+ dto.setDefaultValue(question.getDefaultValue());
+ dto.setContainsPii(question.getContainsPii());
+ dto.setDisplayOrder(question.getDisplayOrder());
+
+ svc.updateQuestion(question.getQuestionId(), dto);
+ }
+}
diff --git a/src/main/java/com/iemr/flw/seeder/migration/V009_BackfillOptionUuid.java b/src/main/java/com/iemr/flw/seeder/migration/V009_BackfillOptionUuid.java
new file mode 100644
index 000000000..bfcb384d2
--- /dev/null
+++ b/src/main/java/com/iemr/flw/seeder/migration/V009_BackfillOptionUuid.java
@@ -0,0 +1,62 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.flw.seeder.migration;
+
+import com.iemr.flw.domain.iemr.QuestionOption;
+import com.iemr.flw.repo.iemr.QuestionOptionRepo;
+import com.iemr.flw.service.DynamicFormReconciliationService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * Backfills t_question_option.optionUuid for every option of every form version created before
+ * the column existed. The uuid is formType + optionValue, so the same option resolves to the same
+ * optionUuid in v1, v2, … — letting clients map answers across versions, as with sectionUuid/questionUuid.
+ * Only touches rows whose optionUuid is still blank, so re-running is a no-op.
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class V009_BackfillOptionUuid implements FormStructureMigration {
+
+ private final QuestionOptionRepo optionRepo;
+
+ @Override
+ public String migrationId() {
+ return "V009";
+ }
+
+ @Override
+ public void apply(DynamicFormReconciliationService svc) {
+ List options = optionRepo.findAllMissingOptionUuid();
+ for (QuestionOption option : options) {
+ option.setOptionUuid(QuestionOption.buildOptionUuid(
+ option.getSectionQuestion().getFormSection().getFormVersion().getDynamicForm().getFormType(),
+ option.getOptionValue()));
+ }
+ optionRepo.saveAll(options);
+ log.info("V009: backfilled optionUuid on {} option(s).", options.size());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/iemr/flw/service/BeneficiaryService.java b/src/main/java/com/iemr/flw/service/BeneficiaryService.java
index e69bc887f..d043e7eea 100644
--- a/src/main/java/com/iemr/flw/service/BeneficiaryService.java
+++ b/src/main/java/com/iemr/flw/service/BeneficiaryService.java
@@ -5,11 +5,14 @@
import com.iemr.flw.dto.iemr.SAMResponseDTO;
import java.util.List;
+import java.util.Map;
public interface BeneficiaryService {
String getBenData(GetBenRequestHandler requestDTO, String authorisation) throws Exception;
+ Map getBeneficiaryRecordsByRegIds(List benRegIds, String authorisation);
+
String saveEyeCheckupVsit(List eyeCheckupRequestDTOS,String token);
List getEyeCheckUpVisit(GetBenRequestHandler request,String token);
diff --git a/src/main/java/com/iemr/flw/service/CampConfigService.java b/src/main/java/com/iemr/flw/service/CampConfigService.java
index 305bf1f05..325b0dabf 100644
--- a/src/main/java/com/iemr/flw/service/CampConfigService.java
+++ b/src/main/java/com/iemr/flw/service/CampConfigService.java
@@ -1,8 +1,14 @@
package com.iemr.flw.service;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
+import java.util.List;
+
/**
* Camp/van identity for this deployment.
*
@@ -17,12 +23,14 @@
* No inline default — every properties file must set stoptb.van.id explicitly, so a forgotten
* config fails loudly at startup instead of silently running unconfigured.
*
- * Scope: vanID (and the VanSerialNo stamping that depends on it) only. parkingPlaceID is out of
- * scope for this change.
+ * The van's parking place and store (facility) come from m_van, the same source MMU uses
+ * (MasterVanRepo.getFacilityID), and are read once and cached since the van never changes.
*/
@Service
public class CampConfigService {
+ private final Logger logger = LoggerFactory.getLogger(CampConfigService.class);
+
@Value("${stoptb.van.id}")
private int vanID;
@@ -31,6 +39,12 @@ public class CampConfigService {
@Value("${stoptb.enforce.vanid}")
private boolean enforceVanID;
+ @PersistenceContext(unitName = "db_iemr")
+ private EntityManager entityManager;
+
+ private volatile Integer parkingPlaceID;
+ private volatile Integer facilityID;
+
public Integer getVanID() {
if (vanID <= 0) {
if (enforceVanID) {
@@ -42,13 +56,51 @@ public Integer getVanID() {
return vanID;
}
+ /**
+ * The configured van (stoptb.van.id) is always used. A vanID sent by the app is ignored, so data and
+ * stock never land on another camp's van/store; a different value is only logged.
+ */
+ public Integer resolveVanID(Integer requestedVanID) {
+ Integer configured = getVanID();
+ if (requestedVanID != null && requestedVanID > 0 && !requestedVanID.equals(configured)) {
+ logger.warn("Ignoring vanID " + requestedVanID + " from request; using stoptb.van.id " + configured);
+ }
+ return configured;
+ }
+
public boolean isCampConfigured() {
return vanID > 0;
}
- // Not sourced from Redis anymore (out of scope for this change) — kept only because
- // callers across StopTBServiceImpl/DiagnosticOrderServiceImpl/etc. still call it.
+ // Parking place of the configured van (m_van.ParkingPlaceID); 0 when the camp is not configured
+ // or the van is not found, which is what callers received before this lookup existed.
public Integer getParkingPlaceID() {
- return 0;
+ if (parkingPlaceID == null) loadVanDetails();
+ return parkingPlaceID != null ? parkingPlaceID : 0;
+ }
+
+ // Store mapped to the configured van (m_van.FacilityID); null when no store is mapped.
+ public Integer getFacilityID() {
+ if (facilityID == null) loadVanDetails();
+ return facilityID;
+ }
+
+ private synchronized void loadVanDetails() {
+ if (!isCampConfigured() || (parkingPlaceID != null && facilityID != null)) return;
+ try {
+ List> rows = entityManager.createNativeQuery(
+ "SELECT ParkingPlaceID, FacilityID FROM db_iemr.m_van WHERE VanID = :vanID AND Deleted = false")
+ .setParameter("vanID", vanID)
+ .getResultList();
+ if (rows.isEmpty()) {
+ logger.warn("stoptb.van.id " + vanID + " not found in m_van");
+ return;
+ }
+ Object[] row = (Object[]) rows.get(0);
+ parkingPlaceID = row[0] != null ? ((Number) row[0]).intValue() : null;
+ facilityID = row[1] != null ? ((Number) row[1]).intValue() : null;
+ } catch (Exception e) {
+ logger.error("Cannot read m_van details for vanID " + vanID, e);
+ }
}
}
diff --git a/src/main/java/com/iemr/flw/service/DiagnosticOrderService.java b/src/main/java/com/iemr/flw/service/DiagnosticOrderService.java
index 0bd5207b8..42fb1c337 100644
--- a/src/main/java/com/iemr/flw/service/DiagnosticOrderService.java
+++ b/src/main/java/com/iemr/flw/service/DiagnosticOrderService.java
@@ -12,18 +12,50 @@
public interface DiagnosticOrderService {
- DiagnosticOrder createAndPushOrder(DiagnosticOrderRequestDto request) throws Exception;
+ DiagnosticOrder createAndPushOrderByUser(DiagnosticOrderRequestDto request, String jwtToken) throws Exception;
+
+ /**
+ * Same create/push logic as createAndPushOrder, attributed to "SYSTEM" instead of a JWT-derived
+ * user. For callers with no authenticated user in context, e.g. the poll scheduler auto-recreating
+ * an order after an invalid result.
+ */
+ DiagnosticOrder createAndPushOrderAsSystem(DiagnosticOrderRequestDto request) throws Exception;
DiagnosticOrderResultDto processResult(DiagnosticOrder order, DiagnosticPollResult result) throws Exception;
+ /**
+ * Polls the vendor once for this order. The order is re-read first and skipped (returns null) if it
+ * is no longer PENDING/IN_PROGRESS, e.g. a manual result landed after the scheduler loaded it.
+ */
DiagnosticPollResult pollOnce(DiagnosticOrder order) throws Exception;
+ /**
+ * Daily cutoff: marks this order CLOSED with the given reason and notifies the vendor, unless it was
+ * resolved (e.g. by a manual result) after the scheduler loaded it.
+ */
+ void expireOrder(DiagnosticOrder order, String reason) throws Exception;
+
DiagnosticOrderResultDto triggerManualPoll(Long beneficiaryId, String orderType, Long visitCode) throws Exception;
DiagnosticOrder retryPoll(Long beneficiaryId, String orderType, Long visitCode) throws Exception;
+ /**
+ * Best-effort: tells the vendor this order is closed out (no result coming), so they stop
+ * tracking it. Never throws — a provider failure is logged and swallowed, since the order's own
+ * CLOSED status is what actually matters locally. No-op if the order was never pushed to a vendor.
+ */
+ void notifyProviderOrderClosed(DiagnosticOrder order, String reason);
+
DiagnosticOrderResultDto getOrderResult(Long beneficiaryId, String orderType, Long visitCode);
+ /**
+ * Orders with their result, keyed by order id (STOP-TB changes feed). Deleted orders are left
+ * out. Each record carries what getBeneficiariesByStatus and getOrderResult give today: the
+ * beneficiary, order type, visit code and status (the app buckets by the latest order per
+ * beneficiary and type), plus the result fields.
+ */
+ java.util.Map> getOrderRecordsByIds(java.util.Collection ids);
+
DiagnosticOrder getOrder(Long beneficiaryId, String orderType, Long visitCode) throws Exception;
List getOrdersByBeneficiaryId(Long beneficiaryId) throws Exception;
@@ -32,5 +64,5 @@ public interface DiagnosticOrderService {
VendorHealthDto checkVendorHealth(String orderType) throws Exception;
- DiagnosticOrderResultDto submitManualResult(ManualDiagnosticResultRequestDto request) throws Exception;
+ DiagnosticOrderResultDto submitManualResult(ManualDiagnosticResultRequestDto request, String jwtToken) throws Exception;
}
diff --git a/src/main/java/com/iemr/flw/service/DynamicFormDefinitionService.java b/src/main/java/com/iemr/flw/service/DynamicFormDefinitionService.java
index c7aae24df..b3e5558c9 100644
--- a/src/main/java/com/iemr/flw/service/DynamicFormDefinitionService.java
+++ b/src/main/java/com/iemr/flw/service/DynamicFormDefinitionService.java
@@ -23,6 +23,7 @@
import com.iemr.flw.dto.iemr.DynamicFormDTO;
import com.iemr.flw.dto.iemr.FormSectionDTO;
+import com.iemr.flw.dto.iemr.LatestFormVersionDTO;
import com.iemr.flw.dto.iemr.OptionConditionDTO;
import com.iemr.flw.dto.iemr.QuestionOptionDTO;
import com.iemr.flw.dto.iemr.QuestionValidationDTO;
@@ -47,7 +48,7 @@ public interface DynamicFormDefinitionService {
/** Returns a specific version's full definition tree (no cache). */
DynamicFormDTO getFormDefinitionByVersion(Long formId, Integer versionNumber);
- /** Returns metadata for all active forms (without full tree). */
+ /** Returns every active form's latest full definition, with each version's full definition in {@code versions}. */
List getAllForms();
/** Sets the form's isActive flag to true. */
@@ -56,4 +57,6 @@ public interface DynamicFormDefinitionService {
/** Sets the form's isActive flag to false and invalidates cache. */
void deactivateForm(Long formId);
+ /** Latest version of every active form: formId, formUuid, formName and current version number. */
+ List getLatestFormVersions();
}
diff --git a/src/main/java/com/iemr/flw/service/DynamicFormReconciliationService.java b/src/main/java/com/iemr/flw/service/DynamicFormReconciliationService.java
new file mode 100644
index 000000000..f558f0078
--- /dev/null
+++ b/src/main/java/com/iemr/flw/service/DynamicFormReconciliationService.java
@@ -0,0 +1,101 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.flw.service;
+
+import com.iemr.flw.domain.iemr.DynamicForm;
+import com.iemr.flw.domain.iemr.FormSection;
+import com.iemr.flw.domain.iemr.FormVersion;
+import com.iemr.flw.domain.iemr.OptionCondition;
+import com.iemr.flw.domain.iemr.QuestionOption;
+import com.iemr.flw.domain.iemr.QuestionValidation;
+import com.iemr.flw.domain.iemr.SectionQuestion;
+import com.iemr.flw.dto.iemr.DynamicFormDTO;
+import com.iemr.flw.dto.iemr.FormSectionDTO;
+import com.iemr.flw.dto.iemr.OptionConditionDTO;
+import com.iemr.flw.dto.iemr.QuestionOptionDTO;
+import com.iemr.flw.dto.iemr.QuestionValidationDTO;
+import com.iemr.flw.dto.iemr.SectionQuestionDTO;
+
+import java.util.Optional;
+
+/**
+ * Idempotent, in-place reconciliation for dynamic form structure — inserts whatever is missing
+ * from the current latest {@link FormVersion} and leaves existing rows untouched. Never creates a
+ * new version (unlike {@link DynamicFormDefinitionService#updateForm}, which always clones one).
+ *
+ * Used both by the startup seeders (walking their full hardcoded DTO tree every run) and by the
+ * granular structure endpoints (a single section/question/option at a time).
+ *
+ * Each {@code ensureX} method operates on exactly one level — callers walk the tree themselves and
+ * call the next level down, since only the caller knows whether it's safe yet to wire cross-sibling
+ * references (see {@link #ensureCondition}).
+ */
+public interface DynamicFormReconciliationService {
+
+ /** Finds the form by formUuid, or creates it. If found inactive, reactivates it (touches no other field). */
+ DynamicForm ensureForm(DynamicFormDTO dto);
+
+ /** Finds the form's isLatest=true version, or creates version 1 if none exists yet. */
+ FormVersion ensureLatestVersion(DynamicForm form);
+
+ /** Finds a section by sectionUuid within the version, or creates it. */
+ FormSection ensureSection(FormVersion version, FormSectionDTO dto);
+
+ /** Finds a question by questionUuid within the section, or creates it (a section's question is "linked" the moment it's created — no separate link step exists). */
+ SectionQuestion ensureQuestion(FormSection section, SectionQuestionDTO dto);
+
+ /** Finds an option by optionValue within the question, or creates it. */
+ QuestionOption ensureOption(SectionQuestion question, QuestionOptionDTO dto);
+
+ /** Finds a validation by (validationType, validationParam) within the question, or creates it — validations carry no natural key of their own. */
+ QuestionValidation ensureValidation(SectionQuestion question, QuestionValidationDTO dto);
+
+ /**
+ * Finds a condition by (actionType, target) within the option, or creates it, resolving
+ * targetQuestionUuid/targetSectionUuid against the given version. Call only once every sibling
+ * section/question this condition might target already exists as a row.
+ */
+ OptionCondition ensureCondition(QuestionOption option, OptionConditionDTO dto, FormVersion version);
+
+ /** Updates a question's scalar fields only (options/validations are managed by their own endpoints). A displayOrder change moves the question among its siblings, shifting them accordingly. */
+ SectionQuestion updateQuestion(Long questionId, SectionQuestionDTO dto);
+
+ /** Soft-deletes (isActive=false) a question — never hard-deleted; historical responses may reference it. */
+ void unlinkQuestion(Long questionId);
+
+ /** Soft-deletes (isActive=false) an option — never hard-deleted; historical responses may reference it. */
+ void removeOption(Long optionId);
+
+ // ── Read-only lookups, for FormStructureMigration classes to locate what they're changing ──
+
+ /** Finds the given form's current latest version, if the form exists at all. */
+ Optional latestVersion(String formUuid);
+
+ /** Finds a section by sectionUuid within the given version. */
+ Optional section(FormVersion version, String sectionUuid);
+
+ /** Finds a question by questionUuid within the given section. */
+ Optional question(FormSection section, String questionUuid);
+
+ /** Finds an option by optionValue within the given question. */
+ Optional option(SectionQuestion question, String optionValue);
+}
diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java
index 1f2255f3e..8265cfafb 100644
--- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java
+++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java
@@ -55,6 +55,12 @@ List getResponsesByBeneficiary(Long beneficiaryId, Integer vill
/** Single response with all nested section and question answers. */
FormResponseDTO getResponseById(Long responseId);
+ /**
+ * Responses with all nested answers, keyed by responseId, as getResponsesByBeneficiary returns
+ * them (STOP-TB changes feed). Ids with no response are left out.
+ */
+ java.util.Map getResponsesByIds(java.util.Collection responseIds);
+
/**
* Submit multiple form responses in one bulk transaction.
* All FormResponse rows are batch-inserted via saveAll() so responseIds are available
diff --git a/src/main/java/com/iemr/flw/service/StopTBPrescriptionService.java b/src/main/java/com/iemr/flw/service/StopTBPrescriptionService.java
new file mode 100644
index 000000000..c61b4d4f9
--- /dev/null
+++ b/src/main/java/com/iemr/flw/service/StopTBPrescriptionService.java
@@ -0,0 +1,721 @@
+package com.iemr.flw.service;
+
+import com.iemr.flw.domain.identity.RMNCHMBeneficiarydetail;
+import com.iemr.flw.domain.identity.RMNCHMBeneficiarymapping;
+import com.iemr.flw.domain.iemr.BenVisitDetail;
+import com.iemr.flw.repo.identity.BeneficiaryRepo;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigInteger;
+import java.time.LocalDate;
+import java.time.Period;
+import java.util.*;
+
+/**
+ * Stop TB nurse OPD: chief complaints, prescription and dispensing written to the same standard
+ * tables MMU/TM and Inventory-API use, inside the caller's transaction.
+ *
+ * - Chief complaints -> t_benchiefcomplaint (one row per complaint, like TM saveBenChiefComplaints)
+ * - Prescription -> t_prescription + t_prescribeddrug (DrugID + QtyPrescribed, like TM doctor save)
+ * - Dispensing -> t_patientissue + t_itemstockexit (ExitType T_PatientIssue) and
+ * t_itemstockentry.QuantityInHand reduced, like Inventory-API issuePatientDrugs.
+ *
+ * Batches are picked by m_itemcategory.IssueType (FEFO / FIFO / LIFO) from non-expired batches that
+ * last the treatment duration, and are locked (SELECT ... FOR UPDATE) so two nurses can never take the
+ * same stock. If any drug is short, nothing is saved (InsufficientStockException rolls back the caller).
+ *
+ * Native SQL only, no new entity mappings: FLW runs with ddl-auto=create.
+ */
+@Service
+public class StopTBPrescriptionService {
+
+ private static final String REFERENCE_PREFIX = "StopTB:";
+
+ @PersistenceContext(unitName = "db_iemr")
+ private EntityManager em;
+
+ @Autowired
+ private CampConfigService campConfigService;
+
+ @Autowired
+ private BeneficiaryRepo beneficiaryRepo;
+
+ // Off by default: save the prescription only (no store/stock check, no patient issue, stock unchanged).
+ // true = check stock and dispense from the camp store.
+ @Value("${stoptb.dispense.enabled:false}")
+ private boolean dispenseEnabled;
+
+ private final Logger logger = LoggerFactory.getLogger(StopTBPrescriptionService.class);
+
+ public static class InsufficientStockException extends RuntimeException {
+ public InsufficientStockException(String message) {
+ super(message);
+ }
+ }
+
+ /** Previous result for a submissionId already saved, or null. Lets the app retry safely. */
+ @Transactional(propagation = Propagation.MANDATORY)
+ public Map findPreviousSubmission(Long beneficiaryRegID, String submissionId) {
+ if (submissionId == null || submissionId.isBlank()) return null;
+ List> rows = em.createNativeQuery("SELECT PatientIssueID, VisitCode, PrescriptionID FROM db_iemr.t_patientissue "
+ + "WHERE BeneficiaryRegID = :ben AND Reference = :ref AND Deleted = false")
+ .setParameter("ben", beneficiaryRegID)
+ .setParameter("ref", REFERENCE_PREFIX + submissionId)
+ .getResultList();
+ if (rows.isEmpty()) return null;
+ Object[] row = (Object[]) rows.get(0);
+ Integer facilityID = campConfigService.getFacilityID();
+
+ List> issued = em.createNativeQuery("SELECT e.ItemID, i.ItemName, SUM(x.Quantity) "
+ + "FROM db_iemr.t_itemstockexit x "
+ + "JOIN db_iemr.t_itemstockentry e ON e.VanSerialNo = x.ItemStockEntryID AND e.FacilityID = x.FacilityID "
+ + "JOIN db_iemr.m_item i ON i.ItemID = e.ItemID "
+ + "WHERE x.ExitType = 'T_PatientIssue' AND x.ExitTypeID = :issueID AND x.FacilityID = :facilityID "
+ + "GROUP BY e.ItemID, i.ItemName")
+ .setParameter("issueID", ((Number) row[0]).longValue())
+ .setParameter("facilityID", facilityID)
+ .getResultList();
+ List