From 024d38420d4b6e366cbbc77795188793f0cf384e Mon Sep 17 00:00:00 2001 From: Soorya Date: Thu, 2 Apr 2026 17:43:51 +0530 Subject: [PATCH 01/13] Draft feature Post, Get and Delete call --- .../bahmnicore/contract/FormDraftRequest.java | 53 +++ .../contract/FormDraftResponse.java | 76 ++++ .../module/bahmnicore/dao/FormDraftDAO.java | 33 ++ .../bahmnicore/dao/impl/FormDraftDaoImpl.java | 69 ++++ .../module/bahmnicore/model/FormDraft.java | 84 +++++ .../security/PrivilegeConstants.java | 1 + .../bahmnicore/service/FormDraftService.java | 41 +++ .../service/impl/FormDraftServiceImpl.java | 326 ++++++++++++++++++ .../src/main/resources/FormDraft.hbm.xml | 44 +++ .../resources/moduleApplicationContext.xml | 36 ++ .../impl/FormDraftServiceImplTest.java | 290 ++++++++++++++++ .../resources/TestingApplicationContext.xml | 1 + .../v1_0/controller/FormDraftController.java | 136 ++++++++ bahmnicore-omod/src/main/resources/config.xml | 1 + .../src/main/resources/liquibase.xml | 93 +++++ 15 files changed, 1284 insertions(+) create mode 100644 bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftRequest.java create mode 100644 bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftResponse.java create mode 100644 bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java create mode 100644 bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java create mode 100644 bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/model/FormDraft.java create mode 100644 bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java create mode 100644 bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java create mode 100644 bahmnicore-api/src/main/resources/FormDraft.hbm.xml create mode 100644 bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java create mode 100644 bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftRequest.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftRequest.java new file mode 100644 index 0000000000..c16239a5d7 --- /dev/null +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftRequest.java @@ -0,0 +1,53 @@ +package org.bahmni.module.bahmnicore.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class FormDraftRequest { + + @JsonProperty + private String patientUuid; + + @JsonProperty + private String providerUuid; + + @JsonProperty + private String encounterUuid; //optional + + @JsonProperty + private String formData; + + public FormDraftRequest() { + } + + public String getPatientUuid() { + return patientUuid; + } + + public void setPatientUuid(String patientUuid) { + this.patientUuid = patientUuid; + } + + public String getProviderUuid() { + return providerUuid; + } + + public void setProviderUuid(String providerUuid) { + this.providerUuid = providerUuid; + } + + public String getEncounterUuid() { + return encounterUuid; + } + + public void setEncounterUuid(String encounterUuid) { + this.encounterUuid = encounterUuid; + } + + public String getFormData() { + return formData; + } + + public void setFormData(String formData) { + this.formData = formData; + } +} diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftResponse.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftResponse.java new file mode 100644 index 0000000000..9d80ea8edd --- /dev/null +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftResponse.java @@ -0,0 +1,76 @@ +package org.bahmni.module.bahmnicore.contract; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Date; + +public class FormDraftResponse { + + @JsonProperty + private String uuid; + + @JsonProperty + private String formData; + + @JsonProperty + private Boolean markedAsSaved; + + @JsonProperty + private Long timestamp; + + public FormDraftResponse() { + } + + public FormDraftResponse(String uuid) { + this.uuid = uuid; + } + + public FormDraftResponse(String uuid, String formData) { + this.uuid = uuid; + this.formData = formData; + } + + public FormDraftResponse(String uuid, String formData, Boolean markedAsSaved) { + this.uuid = uuid; + this.formData = formData; + this.markedAsSaved = markedAsSaved; + } + + public FormDraftResponse(String uuid, String formData, Boolean markedAsSaved, Long timestamp) { + this.uuid = uuid; + this.formData = formData; + this.markedAsSaved = markedAsSaved; + this.timestamp = timestamp; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getFormData() { + return formData; + } + + public void setFormData(String formData) { + this.formData = formData; + } + + public Boolean getMarkedAsSaved() { + return markedAsSaved; + } + + public void setMarkedAsSaved(Boolean markedAsSaved) { + this.markedAsSaved = markedAsSaved; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } +} diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java new file mode 100644 index 0000000000..2aea115f5b --- /dev/null +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java @@ -0,0 +1,33 @@ +package org.bahmni.module.bahmnicore.dao; + +import org.bahmni.module.bahmnicore.model.FormDraft; + +public interface FormDraftDAO { + + /** + * Save a new form draft. + * Each draft gets a unique UUID and is never updated once voided (new drafts are created instead). + * + * @param draft the FormDraft object to save or update + * @return the saved FormDraft object + */ + FormDraft saveOrUpdate(FormDraft draft); + + /** + * Retrieve the latest non-voided form draft for a patient and user. + * + * @param patientId the OpenMRS patient ID + * @param userId the OpenMRS user ID (provider) + * @return the latest non-voided FormDraft object, or null if not found + */ + FormDraft getLatestByPatientAndUser(Integer patientId, Integer userId); + + /** + * Soft delete (void) the latest non-voided form draft for a patient and user. + * Sets voided = true and dateVoided = now, voidedBy = currentUser, voidReason = "Draft deleted" + * + * @param patientId the OpenMRS patient ID + * @param userId the OpenMRS user ID (provider) + */ + void deleteLatestDraft(Integer patientId, Integer userId); +} diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java new file mode 100644 index 0000000000..8826b3e674 --- /dev/null +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java @@ -0,0 +1,69 @@ +package org.bahmni.module.bahmnicore.dao.impl; + +import org.bahmni.module.bahmnicore.dao.FormDraftDAO; +import org.bahmni.module.bahmnicore.model.FormDraft; +import org.hibernate.SessionFactory; +import org.hibernate.query.Query; +import org.openmrs.api.context.Context; +import org.openmrs.api.db.DAOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +public class FormDraftDaoImpl implements FormDraftDAO { + + private static final Logger log = LoggerFactory.getLogger(FormDraftDaoImpl.class); + + private SessionFactory sessionFactory; + + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + @Override + public FormDraft saveOrUpdate(FormDraft draft) throws DAOException { + try { + sessionFactory.getCurrentSession().saveOrUpdate(draft); + return draft; + } catch (Exception e) { + log.error("Error saving or updating form draft", e); + throw new DAOException("Failed to save or update form draft", e); + } + } + + @Override + public FormDraft getLatestByPatientAndUser(Integer patientId, Integer userId) throws DAOException { + try { + Query query = sessionFactory.getCurrentSession() + .createQuery("FROM FormDraft WHERE patient.patientId = :patientId AND user.userId = :userId " + + "AND voided = false ORDER BY dateCreated DESC", FormDraft.class); + query.setParameter("patientId", patientId); + query.setParameter("userId", userId); + query.setMaxResults(1); + return query.uniqueResult(); + } catch (Exception e) { + log.error("Error retrieving latest form draft for patient: " + patientId + ", user: " + userId, e); + throw new DAOException("Failed to retrieve form draft", e); + } + } + + @Override + public void deleteLatestDraft(Integer patientId, Integer userId) throws DAOException { + try { + FormDraft draft = getLatestByPatientAndUser(patientId, userId); + if (draft != null) { + draft.setVoided(true); + draft.setDateVoided(new Date()); + draft.setVoidedBy(Context.getAuthenticatedUser()); + draft.setVoidReason("Draft deleted"); + sessionFactory.getCurrentSession().saveOrUpdate(draft); + } + } catch (DAOException e) { + throw e; + } catch (Exception e) { + log.error("Error deleting latest form draft for patient: " + patientId + ", user: " + userId, e); + throw new DAOException("Failed to delete form draft", e); + } + } +} diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/model/FormDraft.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/model/FormDraft.java new file mode 100644 index 0000000000..373930f980 --- /dev/null +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/model/FormDraft.java @@ -0,0 +1,84 @@ +package org.bahmni.module.bahmnicore.model; + +import org.openmrs.BaseChangeableOpenmrsData; +import org.openmrs.Encounter; +import org.openmrs.Patient; +import org.openmrs.User; + +public class FormDraft extends BaseChangeableOpenmrsData { + + private Integer id; + + private String uuid; + + private Patient patient; + + private Encounter encounter; // nullable — populated once encounter is created + + private User user; + + private String formDataPath; // Path to JSON file on filesystem + + private Boolean markedAsSaved; // Track if draft has been submitted/saved + + public FormDraft() { + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public Patient getPatient() { + return patient; + } + + public void setPatient(Patient patient) { + this.patient = patient; + } + + public Encounter getEncounter() { + return encounter; + } + + public void setEncounter(Encounter encounter) { + this.encounter = encounter; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public String getFormDataPath() { + return formDataPath; + } + + public void setFormDataPath(String formDataPath) { + this.formDataPath = formDataPath; + } + + public Boolean getMarkedAsSaved() { + return markedAsSaved; + } + + public void setMarkedAsSaved(Boolean markedAsSaved) { + this.markedAsSaved = markedAsSaved; + } +} diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/security/PrivilegeConstants.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/security/PrivilegeConstants.java index d81df6da1f..06619428fe 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/security/PrivilegeConstants.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/security/PrivilegeConstants.java @@ -3,4 +3,5 @@ public class PrivilegeConstants { public static final String DELETE_PATIENT_DOCUMENT_PRIVILEGE = "Delete Patient Document"; public static final String IMPORT_CSV_FILE_PRIVILEGE = "Import CSV Files"; + public static final String DELETE_FORM_DRAFT_PRIVILEGE = "Delete Form Draft"; } diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java new file mode 100644 index 0000000000..514ab2611d --- /dev/null +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java @@ -0,0 +1,41 @@ +package org.bahmni.module.bahmnicore.service; + +import org.bahmni.module.bahmnicore.contract.FormDraftRequest; +import org.bahmni.module.bahmnicore.model.FormDraft; + +public interface FormDraftService { + + /** + * Create a new form draft. Each draft gets a unique UUID. + * + * @param request FormDraftRequest containing patient, provider, and form data + * @return the created FormDraft object with generated UUID + */ + FormDraft saveDraft(FormDraftRequest request); + + /** + * Retrieve the latest non-voided form draft for a patient and provider. + * + * @param patientUuid the UUID of the patient + * @param providerUuid the UUID of the provider + * @return the latest FormDraft object if found, null otherwise + */ + FormDraft getDraft(String patientUuid, String providerUuid); + + /** + * Soft delete (void) the latest non-voided form draft for a patient and provider. + * Sets voided=true and updates audit fields (dateVoided, voidedBy, voidReason). + * + * @param patientUuid the UUID of the patient + * @param providerUuid the UUID of the provider + */ + void discardDraft(String patientUuid, String providerUuid); + + /** + * Retrieve form data from file using UTF-8 charset. + * + * @param formDataPath the file path to read from + * @return the form data as a string, or null if file doesn't exist + */ + String getFormData(String formDataPath); +} diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java new file mode 100644 index 0000000000..dcee0e1f44 --- /dev/null +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java @@ -0,0 +1,326 @@ +package org.bahmni.module.bahmnicore.service.impl; + +import org.bahmni.module.bahmnicore.contract.FormDraftRequest; +import org.bahmni.module.bahmnicore.dao.FormDraftDAO; +import org.bahmni.module.bahmnicore.model.FormDraft; +import org.bahmni.module.bahmnicore.service.FormDraftService; +import org.openmrs.api.context.Context; +import org.openmrs.Encounter; +import org.openmrs.Patient; +import org.openmrs.User; +import org.openmrs.api.APIException; +import org.openmrs.api.EncounterService; +import org.openmrs.api.PatientService; +import org.openmrs.api.UserService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Date; +import java.util.UUID; + +@Transactional +public class FormDraftServiceImpl implements FormDraftService { + + private static final Logger log = LoggerFactory.getLogger(FormDraftServiceImpl.class); + private static final String FORM_DRAFTS_SUBDIRECTORY = "form_draft"; + + private FormDraftDAO formDraftDAO; + private PatientService patientService; + private UserService userService; + private EncounterService encounterService; + private User authenticatedUser; // For testing - overrides Context.getAuthenticatedUser() + + // For testing purposes - can be overridden + private String formDraftsBasePath; + + public FormDraftServiceImpl() { + // Initialize with OPENMRS_APPLICATION_DATA_DIRECTORY + String appDataDir = System.getProperty("OPENMRS_APPLICATION_DATA_DIRECTORY"); + if (appDataDir == null || appDataDir.isEmpty()) { + throw new IllegalStateException("OPENMRS_APPLICATION_DATA_DIRECTORY system property not set"); + } + this.formDraftsBasePath = appDataDir + FORM_DRAFTS_SUBDIRECTORY; + } + + @Autowired + public void setFormDraftDAO(FormDraftDAO formDraftDAO) { + this.formDraftDAO = formDraftDAO; + } + + @Autowired(required = false) + public void setPatientService(PatientService patientService) { + this.patientService = patientService; + } + + @Autowired(required = false) + public void setUserService(UserService userService) { + this.userService = userService; + } + + @Autowired(required = false) + public void setEncounterService(EncounterService encounterService) { + this.encounterService = encounterService; + } + + // Package-private setters for testing + protected void setFormDraftsBasePath(String basePath) { + this.formDraftsBasePath = basePath; + } + + protected void setAuthenticatedUser(User user) { + this.authenticatedUser = user; + } + + private User getAuthenticatedUser() { + return authenticatedUser != null ? authenticatedUser : Context.getAuthenticatedUser(); + } + + @Override + public FormDraft saveDraft(FormDraftRequest request) { + try { + validateRequest(request); + + PatientService ps = patientService != null ? patientService : Context.getPatientService(); + Patient patient = ps.getPatientByUuid(request.getPatientUuid()); + if (patient == null) { + throw new APIException("Patient not found with UUID: " + request.getPatientUuid()); + } + + UserService us = userService != null ? userService : Context.getUserService(); + User user = us.getUserByUuid(request.getProviderUuid()); + if (user == null) { + throw new APIException("User/Provider not found with UUID: " + request.getProviderUuid()); + } + + FormDraft draft = formDraftDAO.getLatestByPatientAndUser(patient.getPatientId(), user.getUserId()); + boolean isNewDraft = (draft == null); + boolean contentChanged = true; + + if (draft == null) { + draft = new FormDraft(); + draft.setUuid(UUID.randomUUID().toString()); + draft.setDateCreated(new Date()); + draft.setCreator(getAuthenticatedUser()); + } else { + contentChanged = hasFormDataChanged(draft.getFormDataPath(), request.getFormData()); + if (contentChanged) { + draft.setDateChanged(new Date()); + draft.setChangedBy(getAuthenticatedUser()); + } + } + + draft.setPatient(patient); + draft.setUser(user); + + if (request.getEncounterUuid() != null && !request.getEncounterUuid().isEmpty()) { + EncounterService es = encounterService != null ? encounterService : Context.getEncounterService(); + Encounter encounter = es.getEncounterByUuid(request.getEncounterUuid()); + if (encounter != null) { + draft.setEncounter(encounter); + } else { + log.warn("Encounter UUID provided but not found: " + request.getEncounterUuid()); + } + } + + String filePath = generateFilePath(draft.getUuid()); + if (isNewDraft || contentChanged) { + writeFormDataToFile(filePath, request.getFormData()); + } + draft.setFormDataPath(filePath); + + return formDraftDAO.saveOrUpdate(draft); + + } catch (IllegalArgumentException e) { + throw e; + } catch (APIException e) { + throw e; + } catch (IOException e) { + log.error("Error writing form draft file", e); + throw new RuntimeException("Failed to save form draft file: " + e.getMessage(), e); + } catch (Exception e) { + log.error("Error saving form draft", e); + throw new RuntimeException("Failed to save form draft: " + e.getMessage(), e); + } + } + + /** + * Check if the form data content has changed by comparing with existing file. + * Returns true if content differs or file doesn't exist. + */ + private boolean hasFormDataChanged(String filePath, String newFormData) { + if (filePath == null || newFormData == null) { + return true; + } + + try { + File file = new File(filePath); + if (!file.exists()) { + return true; // File doesn't exist, so content is new + } + + String existingContent = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + return !existingContent.equals(newFormData); + } catch (IOException e) { + log.warn("Error reading existing form data file, assuming content changed", e); + return true; // If we can't read, assume it changed to be safe + } + } + + /** + * Validate that required fields are present and not empty. + */ + private void validateRequest(FormDraftRequest request) { + if (request.getPatientUuid() == null || request.getPatientUuid().isEmpty()) { + throw new IllegalArgumentException("Patient UUID is required"); + } + if (request.getProviderUuid() == null || request.getProviderUuid().isEmpty()) { + throw new IllegalArgumentException("Provider UUID is required"); + } + if (request.getFormData() == null || request.getFormData().isEmpty()) { + throw new IllegalArgumentException("Form data is required"); + } + } + + /** + * Generate file path for form draft data using UUID. + * Format: {OPENMRS_APPLICATION_DATA_DIRECTORY}/form_draft/{draftUuid}.json + */ + private String generateFilePath(String draftUuid) { + return String.format("%s%s%s.json", + formDraftsBasePath, + File.separator, + draftUuid); + } + + /** + * Write form data JSON to file atomically. + * Uses temp file + rename to ensure consistency. + */ + private void writeFormDataToFile(String filePath, String formData) throws IOException { + File targetFile = new File(filePath); + File parentDir = targetFile.getParentFile(); + + if (!parentDir.exists()) { + if (!parentDir.mkdirs()) { + throw new IOException("Failed to create directory: " + parentDir.getAbsolutePath()); + } + } + + String tempPath = filePath + ".tmp"; + File tempFile = new File(tempPath); + + try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(tempFile), StandardCharsets.UTF_8)) { + writer.write(formData); + writer.flush(); + } catch (IOException e) { + tempFile.delete(); + throw e; + } + + if (!tempFile.renameTo(targetFile)) { + tempFile.delete(); + throw new IOException("Failed to finalize form data file: " + filePath); + } + } + + /** + * Retrieve the latest non-voided form draft for a patient and provider. + */ + @Override + public FormDraft getDraft(String patientUuid, String providerUuid) { + try { + // Validate required fields + if (patientUuid == null || patientUuid.isEmpty()) { + throw new IllegalArgumentException("Patient UUID is required"); + } + if (providerUuid == null || providerUuid.isEmpty()) { + throw new IllegalArgumentException("Provider UUID is required"); + } + + // Fetch entities to get their IDs + PatientService ps = patientService != null ? patientService : Context.getPatientService(); + Patient patient = ps.getPatientByUuid(patientUuid); + if (patient == null) { + return null; + } + + UserService us = userService != null ? userService : Context.getUserService(); + User user = us.getUserByUuid(providerUuid); + if (user == null) { + return null; + } + + // Query by patient ID and user ID + return formDraftDAO.getLatestByPatientAndUser(patient.getPatientId(), user.getUserId()); + + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + log.error("Error retrieving form draft", e); + throw new RuntimeException("Failed to retrieve form draft: " + e.getMessage(), e); + } + } + + @Override + public void discardDraft(String patientUuid, String providerUuid) { + try { + // Validate required fields + if (patientUuid == null || patientUuid.isEmpty()) { + throw new IllegalArgumentException("Patient UUID is required"); + } + if (providerUuid == null || providerUuid.isEmpty()) { + throw new IllegalArgumentException("Provider UUID is required"); + } + + // Fetch entities to get their IDs + PatientService ps = patientService != null ? patientService : Context.getPatientService(); + Patient patient = ps.getPatientByUuid(patientUuid); + if (patient == null) { + throw new APIException("Patient not found with UUID: " + patientUuid); + } + + UserService us = userService != null ? userService : Context.getUserService(); + User user = us.getUserByUuid(providerUuid); + if (user == null) { + throw new APIException("User/Provider not found with UUID: " + providerUuid); + } + + // Delete (void) latest draft for this patient-provider pair + formDraftDAO.deleteLatestDraft(patient.getPatientId(), user.getUserId()); + + } catch (IllegalArgumentException e) { + throw e; + } catch (APIException e) { + throw e; + } catch (Exception e) { + log.error("Error discarding form draft", e); + throw new RuntimeException("Failed to discard form draft: " + e.getMessage(), e); + } + } + + @Override + public String getFormData(String formDataPath) { + if (formDataPath == null) { + return null; + } + + try { + File file = new File(formDataPath); + if (!file.exists()) { + return null; + } + return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + } catch (IOException e) { + log.warn("Error reading form data file: " + formDataPath, e); + return null; + } + } +} diff --git a/bahmnicore-api/src/main/resources/FormDraft.hbm.xml b/bahmnicore-api/src/main/resources/FormDraft.hbm.xml new file mode 100644 index 0000000000..93ce08a22b --- /dev/null +++ b/bahmnicore-api/src/main/resources/FormDraft.hbm.xml @@ -0,0 +1,44 @@ + + + + + + + + + draft_id_seq + + + + + + + + + + + + + + + + + + + diff --git a/bahmnicore-api/src/main/resources/moduleApplicationContext.xml b/bahmnicore-api/src/main/resources/moduleApplicationContext.xml index 65f1a81634..d557cf3919 100644 --- a/bahmnicore-api/src/main/resources/moduleApplicationContext.xml +++ b/bahmnicore-api/src/main/resources/moduleApplicationContext.xml @@ -310,4 +310,40 @@ + + + + + org.bahmni.module.bahmnicore.service.FormDraftService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java b/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java new file mode 100644 index 0000000000..3f4ac6ada9 --- /dev/null +++ b/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java @@ -0,0 +1,290 @@ +package org.bahmni.module.bahmnicore.service.impl; + +import org.bahmni.module.bahmnicore.contract.FormDraftRequest; +import org.bahmni.module.bahmnicore.dao.FormDraftDAO; +import org.bahmni.module.bahmnicore.model.FormDraft; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.openmrs.Encounter; +import org.openmrs.Patient; +import org.openmrs.User; +import org.openmrs.api.EncounterService; +import org.openmrs.api.PatientService; +import org.openmrs.api.UserService; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +public class FormDraftServiceImplTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Mock + private FormDraftDAO formDraftDAO; + + @Mock + private PatientService patientService; + + @Mock + private UserService userService; + + @Mock + private EncounterService encounterService; + + private FormDraftServiceImpl formDraftService; + + private static final String PATIENT_UUID = "patient-uuid-123"; + private static final int PATIENT_ID = 1; + private static final String PROVIDER_UUID = "provider-uuid-456"; + private static final int PROVIDER_ID = 2; + private static final String ENCOUNTER_UUID = "encounter-uuid-789"; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + // Set OPENMRS_APPLICATION_DATA_DIRECTORY for test environment + System.setProperty("OPENMRS_APPLICATION_DATA_DIRECTORY", temporaryFolder.getRoot().getAbsolutePath()); + + formDraftService = new FormDraftServiceImpl(); + formDraftService.setFormDraftDAO(formDraftDAO); + formDraftService.setPatientService(patientService); + formDraftService.setUserService(userService); + formDraftService.setEncounterService(encounterService); + + // Set authenticated user for testing + User mockUser = new User(); + mockUser.setUuid("user-uuid"); + formDraftService.setAuthenticatedUser(mockUser); + } + + @After + public void tearDown() { + // Clean up system property + System.clearProperty("OPENMRS_APPLICATION_DATA_DIRECTORY"); + } + + @Test + public void saveDraft_shouldCreateNewDraftWhenNoneExists() { + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null, "{\"form\":\"data\"}"); + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); + when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); + + FormDraft result = formDraftService.saveDraft(request); + + assertNotNull(result); + assertNotNull(result.getUuid()); + assertEquals(patient, result.getPatient()); + assertEquals(user, result.getUser()); + assertNotNull(result.getDateCreated()); + assertNull(result.getDateChanged()); + + verify(formDraftDAO).saveOrUpdate(any(FormDraft.class)); + } + + @Test + public void saveDraft_shouldUpdateExistingDraftForSamePatientProvider() { + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null, "{\"updated\":\"data\"}"); + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + + FormDraft existingDraft = new FormDraft(); + existingDraft.setUuid("existing-uuid"); + existingDraft.setPatient(patient); + existingDraft.setUser(user); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(existingDraft); + when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); + + FormDraft result = formDraftService.saveDraft(request); + + assertEquals("existing-uuid", result.getUuid()); + assertNotNull(result.getDateChanged()); + verify(formDraftDAO).saveOrUpdate(existingDraft); + } + + @Test + public void saveDraft_shouldSetEncounterWhenEncounterUuidIsProvided() { + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, ENCOUNTER_UUID, "{\"form\":\"data\"}"); + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + Encounter encounter = new Encounter(); + encounter.setUuid(ENCOUNTER_UUID); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); + when(encounterService.getEncounterByUuid(ENCOUNTER_UUID)).thenReturn(encounter); + when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); + + FormDraft result = formDraftService.saveDraft(request); + + assertEquals(encounter, result.getEncounter()); + } + + @Test + public void saveDraft_shouldNotFailWhenEncounterUuidNotFound() { + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, "nonexistent-encounter", "{\"form\":\"data\"}"); + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); + when(encounterService.getEncounterByUuid("nonexistent-encounter")).thenReturn(null); + when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); + + FormDraft result = formDraftService.saveDraft(request); + + assertNull(result.getEncounter()); + } + + @Test(expected = IllegalArgumentException.class) + public void saveDraft_shouldThrowWhenPatientUuidIsNull() { + FormDraftRequest request = buildRequest(null, PROVIDER_UUID, null, "{\"form\":\"data\"}"); + formDraftService.saveDraft(request); + } + + @Test(expected = IllegalArgumentException.class) + public void saveDraft_shouldThrowWhenProviderUuidIsEmpty() { + FormDraftRequest request = buildRequest(PATIENT_UUID, "", null, "{\"form\":\"data\"}"); + formDraftService.saveDraft(request); + } + + @Test(expected = IllegalArgumentException.class) + public void saveDraft_shouldThrowWhenFormDataIsNull() { + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null, null); + formDraftService.saveDraft(request); + } + + @Test + public void saveDraft_shouldPersistFormDataPath() { + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null, "{\"form\":\"data\"}"); + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); + + ArgumentCaptor captor = ArgumentCaptor.forClass(FormDraft.class); + when(formDraftDAO.saveOrUpdate(captor.capture())).thenAnswer(inv -> inv.getArguments()[0]); + + formDraftService.saveDraft(request); + + FormDraft saved = captor.getValue(); + assertNotNull(saved.getFormDataPath()); + assertTrue(saved.getFormDataPath().endsWith(".json")); + assertTrue(saved.getFormDataPath().contains(saved.getUuid())); + } + + @Test + public void getDraft_shouldReturnDraftForValidPatientAndProvider() { + FormDraft existingDraft = new FormDraft(); + existingDraft.setUuid("draft-uuid"); + existingDraft.setFormDataPath("/path/to/draft.json"); + + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(existingDraft); + + FormDraft result = formDraftService.getDraft(PATIENT_UUID, PROVIDER_UUID); + + assertNotNull(result); + assertEquals("draft-uuid", result.getUuid()); + } + + @Test + public void getDraft_shouldReturnNullWhenNoDraftExists() { + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); + + FormDraft result = formDraftService.getDraft(PATIENT_UUID, PROVIDER_UUID); + + assertNull(result); + } + + @Test(expected = IllegalArgumentException.class) + public void getDraft_shouldThrowWhenPatientUuidIsNull() { + formDraftService.getDraft(null, PROVIDER_UUID); + } + + @Test(expected = IllegalArgumentException.class) + public void getDraft_shouldThrowWhenProviderUuidIsEmpty() { + formDraftService.getDraft(PATIENT_UUID, ""); + } + + @Test + public void discardDraft_shouldCallDaoDeleteLatestDraft() { + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + + formDraftService.discardDraft(PATIENT_UUID, PROVIDER_UUID); + + verify(formDraftDAO).deleteLatestDraft(PATIENT_ID, PROVIDER_ID); + } + + @Test(expected = IllegalArgumentException.class) + public void discardDraft_shouldThrowWhenPatientUuidIsNull() { + formDraftService.discardDraft(null, PROVIDER_UUID); + } + + @Test(expected = IllegalArgumentException.class) + public void discardDraft_shouldThrowWhenProviderUuidIsEmpty() { + formDraftService.discardDraft(PATIENT_UUID, ""); + } + + // --- Helpers --- + + private FormDraftRequest buildRequest(String patientUuid, String providerUuid, String encounterUuid, String formData) { + FormDraftRequest request = new FormDraftRequest(); + request.setPatientUuid(patientUuid); + request.setProviderUuid(providerUuid); + request.setEncounterUuid(encounterUuid); + request.setFormData(formData); + return request; + } + + private Patient buildPatient(String uuid, int patientId) { + Patient patient = new Patient(); + patient.setUuid(uuid); + patient.setPatientId(patientId); + return patient; + } + + private User buildUser(String uuid, int userId) { + User user = new User(); + user.setUuid(uuid); + user.setUserId(userId); + return user; + } +} diff --git a/bahmnicore-api/src/test/resources/TestingApplicationContext.xml b/bahmnicore-api/src/test/resources/TestingApplicationContext.xml index b7b812da9a..88ef8821d0 100644 --- a/bahmnicore-api/src/test/resources/TestingApplicationContext.xml +++ b/bahmnicore-api/src/test/resources/TestingApplicationContext.xml @@ -27,6 +27,7 @@ AddressHierarchyEntry.hbm.xml AddressHierarchyLevel.hbm.xml + FormDraft.hbm.xml diff --git a/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java b/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java new file mode 100644 index 0000000000..2b50767c1f --- /dev/null +++ b/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java @@ -0,0 +1,136 @@ +package org.bahmni.module.bahmnicore.web.v1_0.controller; + +import org.bahmni.module.bahmnicore.contract.FormDraftRequest; +import org.bahmni.module.bahmnicore.contract.FormDraftResponse; +import org.bahmni.module.bahmnicore.model.FormDraft; +import org.bahmni.module.bahmnicore.security.PrivilegeConstants; +import org.bahmni.module.bahmnicore.service.FormDraftService; +import org.bahmni.module.bahmnicore.util.WebUtils; +import org.openmrs.api.context.Context; +import org.openmrs.module.webservices.rest.web.RestConstants; +import org.openmrs.module.webservices.rest.web.v1_0.controller.BaseRestController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/bahmnicore/formdraft") +public class FormDraftController extends BaseRestController { + + private static final Logger log = LoggerFactory.getLogger(FormDraftController.class); + + @Autowired + private FormDraftService formDraftService; + + /** + * Auto-save a form draft. Upserts by patient and provider UUID. + * POST /rest/v1/bahmnicore/formdraft + * + * @param request FormDraftRequest with patientUuid, providerUuid, and formData + * @return FormDraftResponse with uuid, formData, markedAsSaved flag, and timestamp + */ + @RequestMapping(method = RequestMethod.POST) + @ResponseBody + public ResponseEntity saveDraft(@RequestBody FormDraftRequest request) { + try { + FormDraft draft = formDraftService.saveDraft(request); + String formData = formDraftService.getFormData(draft.getFormDataPath()); + Long timestamp = draft.getDateChanged() != null ? draft.getDateChanged().getTime() : draft.getDateCreated().getTime(); + FormDraftResponse response = new FormDraftResponse(draft.getUuid(), formData, draft.getMarkedAsSaved(), timestamp); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (IllegalArgumentException e) { + log.warn("Invalid form draft request", e); + return new ResponseEntity<>( + WebUtils.wrapErrorResponse(null, e.getMessage()), + HttpStatus.BAD_REQUEST); + } catch (Exception e) { + log.error("Error saving form draft", e); + return new ResponseEntity<>( + WebUtils.wrapErrorResponse(null, e.getMessage()), + HttpStatus.BAD_REQUEST); + } + } + + /** + * Retrieve a form draft by patient and provider UUIDs. + * GET /rest/v1/bahmnicore/formdraft?patientUuid=xxx&providerUuid=yyy + * + * @param patientUuid the UUID of the patient + * @param providerUuid the UUID of the provider + * @return FormDraftResponse with uuid, formData, and timestamp + */ + @RequestMapping(method = RequestMethod.GET) + @ResponseBody + public ResponseEntity getDraft( + @RequestParam(value = "patientUuid", required = true) String patientUuid, + @RequestParam(value = "providerUuid", required = true) String providerUuid) { + try { + FormDraft draft = formDraftService.getDraft(patientUuid, providerUuid); + if (draft == null) { + return new ResponseEntity<>( + WebUtils.wrapErrorResponse(null, "No draft found for this patient and provider"), + HttpStatus.NOT_FOUND); + } + + String formData = formDraftService.getFormData(draft.getFormDataPath()); + Long timestamp = draft.getDateChanged() != null ? draft.getDateChanged().getTime() : draft.getDateCreated().getTime(); + FormDraftResponse response = new FormDraftResponse(draft.getUuid(), formData, draft.getMarkedAsSaved(), timestamp); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (IllegalArgumentException e) { + log.warn("Invalid form draft request", e); + return new ResponseEntity<>( + WebUtils.wrapErrorResponse(null, e.getMessage()), + HttpStatus.BAD_REQUEST); + } catch (Exception e) { + log.error("Error retrieving form draft", e); + return new ResponseEntity<>( + WebUtils.wrapErrorResponse(null, e.getMessage()), + HttpStatus.BAD_REQUEST); + } + } + + /** + * Discard (void) a form draft by patient and provider UUIDs. + * DELETE /rest/v1/bahmnicore/formdraft?patientUuid=xxx&providerUuid=yyy + * + * @param patientUuid the UUID of the patient + * @param providerUuid the UUID of the provider + * @return 204 No Content on success, 403 Forbidden if insufficient privileges + */ + @RequestMapping(method = RequestMethod.DELETE) + @ResponseBody + public ResponseEntity discardDraft( + @RequestParam(value = "patientUuid", required = true) String patientUuid, + @RequestParam(value = "providerUuid", required = true) String providerUuid) { + if (!Context.getUserContext().hasPrivilege(PrivilegeConstants.DELETE_FORM_DRAFT_PRIVILEGE)) { + log.error("User " + Context.getAuthenticatedUser().getUsername() + + " does not have privilege to discard form drafts"); + return new ResponseEntity<>( + WebUtils.wrapErrorResponse(null, "Insufficient privileges to discard form draft"), + HttpStatus.FORBIDDEN); + } + try { + formDraftService.discardDraft(patientUuid, providerUuid); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } catch (IllegalArgumentException e) { + log.warn("Invalid form draft request", e); + return new ResponseEntity<>( + WebUtils.wrapErrorResponse(null, e.getMessage()), + HttpStatus.BAD_REQUEST); + } catch (Exception e) { + log.error("Error discarding form draft", e); + return new ResponseEntity<>( + WebUtils.wrapErrorResponse(null, e.getMessage()), + HttpStatus.BAD_REQUEST); + } + } + +} diff --git a/bahmnicore-omod/src/main/resources/config.xml b/bahmnicore-omod/src/main/resources/config.xml index d3915522b2..e3848a269f 100644 --- a/bahmnicore-omod/src/main/resources/config.xml +++ b/bahmnicore-omod/src/main/resources/config.xml @@ -146,6 +146,7 @@ EntityMappingType.hbm.xml Notes.hbm.xml NoteType.hbm.xml + FormDraft.hbm.xml diff --git a/bahmnicore-omod/src/main/resources/liquibase.xml b/bahmnicore-omod/src/main/resources/liquibase.xml index 5a6e15aa1a..e4f124bc71 100644 --- a/bahmnicore-omod/src/main/resources/liquibase.xml +++ b/bahmnicore-omod/src/main/resources/liquibase.xml @@ -4713,4 +4713,97 @@ + + + + + + + Create form_draft table for auto-save functionality in observation forms + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM privilege WHERE privilege = 'Delete Form Draft' + + + Add Delete Form Draft privilege for discarding form drafts + + + + + + + From b3d9e0bdc4f7804443edc0341fe42737dda5ed7d Mon Sep 17 00:00:00 2001 From: Soorya Kumaran C <90232857+SooryaKumaranC-tw@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:45:56 +0530 Subject: [PATCH 02/13] Merge pull request #23 from cureinternational/draft-form Soorya | 109059: Integrate Auto-save functionality into Observation Forms --- .../resources/moduleApplicationContext.xml | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/bahmnicore-api/src/main/resources/moduleApplicationContext.xml b/bahmnicore-api/src/main/resources/moduleApplicationContext.xml index d557cf3919..81f42bf862 100644 --- a/bahmnicore-api/src/main/resources/moduleApplicationContext.xml +++ b/bahmnicore-api/src/main/resources/moduleApplicationContext.xml @@ -346,4 +346,40 @@ + + + + + org.bahmni.module.bahmnicore.service.FormDraftService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 26fe1054446f1a12f9de50f52c48f7703a179a3d Mon Sep 17 00:00:00 2001 From: Pooja Sastry Date: Thu, 16 Apr 2026 14:05:56 +0530 Subject: [PATCH 03/13] Pooja | Hive-109060 | Add function to update markAsSaved flag, change response in GET call when no drafts found --- .../bahmnicore/service/FormDraftService.java | 9 ++++ .../service/impl/FormDraftServiceImpl.java | 50 +++++++++++++++++++ .../v1_0/controller/FormDraftController.java | 34 +++++++++++-- 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java index 514ab2611d..e914d2577a 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java @@ -38,4 +38,13 @@ public interface FormDraftService { * @return the form data as a string, or null if file doesn't exist */ String getFormData(String formDataPath); + + /** + * Mark the latest form draft as saved for a patient and provider. + * Sets markedAsSaved=true so subsequent saves will create a new draft. + * + * @param patientUuid the UUID of the patient + * @param providerUuid the UUID of the provider + */ + void markDraftAsSaved(String patientUuid, String providerUuid); } diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java index dcee0e1f44..2b0b410f12 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java @@ -104,11 +104,18 @@ public FormDraft saveDraft(FormDraftRequest request) { boolean isNewDraft = (draft == null); boolean contentChanged = true; + // If the latest draft is marked as saved, create a new draft instead of updating + if (draft != null && draft.getMarkedAsSaved() != null && draft.getMarkedAsSaved()) { + isNewDraft = true; + draft = null; + } + if (draft == null) { draft = new FormDraft(); draft.setUuid(UUID.randomUUID().toString()); draft.setDateCreated(new Date()); draft.setCreator(getAuthenticatedUser()); + draft.setMarkedAsSaved(false); } else { contentChanged = hasFormDataChanged(draft.getFormDataPath(), request.getFormData()); if (contentChanged) { @@ -323,4 +330,47 @@ public String getFormData(String formDataPath) { return null; } } + + @Override + public void markDraftAsSaved(String patientUuid, String providerUuid) { + try { + // Validate required fields + if (patientUuid == null || patientUuid.isEmpty()) { + throw new IllegalArgumentException("Patient UUID is required"); + } + if (providerUuid == null || providerUuid.isEmpty()) { + throw new IllegalArgumentException("Provider UUID is required"); + } + + // Fetch entities to get their IDs + PatientService ps = patientService != null ? patientService : Context.getPatientService(); + Patient patient = ps.getPatientByUuid(patientUuid); + if (patient == null) { + throw new APIException("Patient not found with UUID: " + patientUuid); + } + + UserService us = userService != null ? userService : Context.getUserService(); + User user = us.getUserByUuid(providerUuid); + if (user == null) { + throw new APIException("User/Provider not found with UUID: " + providerUuid); + } + + // Get latest draft and mark as saved + FormDraft draft = formDraftDAO.getLatestByPatientAndUser(patient.getPatientId(), user.getUserId()); + if (draft != null) { + draft.setMarkedAsSaved(true); + draft.setDateChanged(new Date()); + draft.setChangedBy(getAuthenticatedUser()); + formDraftDAO.saveOrUpdate(draft); + } + + } catch (IllegalArgumentException e) { + throw e; + } catch (APIException e) { + throw e; + } catch (Exception e) { + log.error("Error marking form draft as saved", e); + throw new RuntimeException("Failed to mark form draft as saved: " + e.getMessage(), e); + } + } } diff --git a/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java b/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java index 2b50767c1f..386326ffe0 100644 --- a/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java +++ b/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java @@ -75,9 +75,7 @@ public ResponseEntity getDraft( try { FormDraft draft = formDraftService.getDraft(patientUuid, providerUuid); if (draft == null) { - return new ResponseEntity<>( - WebUtils.wrapErrorResponse(null, "No draft found for this patient and provider"), - HttpStatus.NOT_FOUND); + return new ResponseEntity<>(new FormDraftResponse(), HttpStatus.OK); } String formData = formDraftService.getFormData(draft.getFormDataPath()); @@ -97,6 +95,36 @@ public ResponseEntity getDraft( } } + /** + * Mark a form draft as saved (finalized). + * PATCH /rest/v1/bahmnicore/formdraft?patientUuid=xxx&providerUuid=yyy + * + * @param patientUuid the UUID of the patient + * @param providerUuid the UUID of the provider + * @return 200 OK on success + */ + @RequestMapping(method = RequestMethod.PATCH) + @ResponseBody + public ResponseEntity markDraftAsSaved( + @RequestParam(value = "patientUuid", required = true) String patientUuid, + @RequestParam(value = "providerUuid", required = true) String providerUuid) { + try { + formDraftService.markDraftAsSaved(patientUuid, providerUuid); + log.info("Draft marked as saved for patient: " + patientUuid + " and provider: " + providerUuid); + return new ResponseEntity<>(HttpStatus.OK); + } catch (IllegalArgumentException e) { + log.warn("Invalid form draft request", e); + return new ResponseEntity<>( + WebUtils.wrapErrorResponse(null, e.getMessage()), + HttpStatus.BAD_REQUEST); + } catch (Exception e) { + log.error("Error marking draft as saved", e); + return new ResponseEntity<>( + WebUtils.wrapErrorResponse(null, e.getMessage()), + HttpStatus.BAD_REQUEST); + } + } + /** * Discard (void) a form draft by patient and provider UUIDs. * DELETE /rest/v1/bahmnicore/formdraft?patientUuid=xxx&providerUuid=yyy From 8567fac8a7759bd8170181cdc7347bdc02796e9b Mon Sep 17 00:00:00 2001 From: Pooja Sastry Date: Fri, 17 Apr 2026 10:46:28 +0530 Subject: [PATCH 04/13] Pooja | Hive-110920 | Update tests and update Claude.md file --- .../impl/FormDraftServiceImplTest.java | 123 ++++++++++++++++ .../controller/FormDraftControllerTest.java | 131 ++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 bahmnicore-omod/src/test/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftControllerTest.java diff --git a/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java b/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java index 3f4ac6ada9..916e893ff7 100644 --- a/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java +++ b/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java @@ -14,14 +14,17 @@ import org.openmrs.Encounter; import org.openmrs.Patient; import org.openmrs.User; +import org.openmrs.api.APIException; import org.openmrs.api.EncounterService; import org.openmrs.api.PatientService; import org.openmrs.api.UserService; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertNotEquals; import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; import static org.mockito.Mockito.verify; @@ -263,6 +266,126 @@ public void discardDraft_shouldThrowWhenProviderUuidIsEmpty() { formDraftService.discardDraft(PATIENT_UUID, ""); } + @Test + public void markDraftAsSaved_shouldUpdateDraftMarkedAsSavedFlag() { + FormDraft existingDraft = new FormDraft(); + existingDraft.setUuid("draft-uuid"); + existingDraft.setMarkedAsSaved(false); + + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(existingDraft); + when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); + + formDraftService.markDraftAsSaved(PATIENT_UUID, PROVIDER_UUID); + + ArgumentCaptor captor = ArgumentCaptor.forClass(FormDraft.class); + verify(formDraftDAO).saveOrUpdate(captor.capture()); + + FormDraft updatedDraft = captor.getValue(); + assertTrue(updatedDraft.getMarkedAsSaved()); + assertNotNull(updatedDraft.getDateChanged()); + } + + @Test + public void markDraftAsSaved_shouldDoNothingWhenNoDraftExists() { + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); + + formDraftService.markDraftAsSaved(PATIENT_UUID, PROVIDER_UUID); + + verify(formDraftDAO, org.mockito.Mockito.never()).saveOrUpdate(any(FormDraft.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void markDraftAsSaved_shouldThrowWhenPatientUuidIsNull() { + formDraftService.markDraftAsSaved(null, PROVIDER_UUID); + } + + @Test(expected = IllegalArgumentException.class) + public void markDraftAsSaved_shouldThrowWhenPatientUuidIsEmpty() { + formDraftService.markDraftAsSaved("", PROVIDER_UUID); + } + + @Test(expected = IllegalArgumentException.class) + public void markDraftAsSaved_shouldThrowWhenProviderUuidIsNull() { + formDraftService.markDraftAsSaved(PATIENT_UUID, null); + } + + @Test(expected = IllegalArgumentException.class) + public void markDraftAsSaved_shouldThrowWhenProviderUuidIsEmpty() { + formDraftService.markDraftAsSaved(PATIENT_UUID, ""); + } + + @Test(expected = APIException.class) + public void markDraftAsSaved_shouldThrowWhenPatientNotFound() { + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(null); + + formDraftService.markDraftAsSaved(PATIENT_UUID, PROVIDER_UUID); + } + + @Test(expected = APIException.class) + public void markDraftAsSaved_shouldThrowWhenProviderNotFound() { + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(null); + + formDraftService.markDraftAsSaved(PATIENT_UUID, PROVIDER_UUID); + } + + @Test + public void saveDraft_shouldCreateNewDraftWhenExistingDraftIsMarkedAsSaved() { + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null, "{\"updated\":\"data\"}"); + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + + FormDraft markedDraft = new FormDraft(); + markedDraft.setUuid("marked-draft-uuid"); + markedDraft.setPatient(patient); + markedDraft.setUser(user); + markedDraft.setMarkedAsSaved(true); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(markedDraft); + when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); + + FormDraft result = formDraftService.saveDraft(request); + + // Should create a new draft instead of updating the marked one + assertNotNull(result.getUuid()); + assertNotEquals("marked-draft-uuid", result.getUuid()); + assertFalse(result.getMarkedAsSaved()); + verify(formDraftDAO).saveOrUpdate(any(FormDraft.class)); + } + + @Test + public void saveDraft_shouldInitializeMarkedAsSavedAsFalseForNewDraft() { + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null, "{\"form\":\"data\"}"); + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); + + ArgumentCaptor captor = ArgumentCaptor.forClass(FormDraft.class); + when(formDraftDAO.saveOrUpdate(captor.capture())).thenAnswer(inv -> inv.getArguments()[0]); + + formDraftService.saveDraft(request); + + FormDraft saved = captor.getValue(); + assertFalse(saved.getMarkedAsSaved()); + } + // --- Helpers --- private FormDraftRequest buildRequest(String patientUuid, String providerUuid, String encounterUuid, String formData) { diff --git a/bahmnicore-omod/src/test/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftControllerTest.java b/bahmnicore-omod/src/test/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftControllerTest.java new file mode 100644 index 0000000000..d610c3acb5 --- /dev/null +++ b/bahmnicore-omod/src/test/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftControllerTest.java @@ -0,0 +1,131 @@ +package org.bahmni.module.bahmnicore.web.v1_0.controller; + +import org.bahmni.module.bahmnicore.contract.FormDraftRequest; +import org.bahmni.module.bahmnicore.contract.FormDraftResponse; +import org.bahmni.module.bahmnicore.model.FormDraft; +import org.bahmni.module.bahmnicore.service.FormDraftService; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.Date; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class FormDraftControllerTest { + + private FormDraftController controller; + private FormDraftService formDraftService; + + private static final String PATIENT_UUID = "patient-uuid-123"; + private static final String PROVIDER_UUID = "provider-uuid-456"; + private static final String DRAFT_UUID = "draft-uuid"; + private static final String FORM_DATA_PATH = "/path/to/draft.json"; + + @Before + public void setUp() throws Exception { + formDraftService = mock(FormDraftService.class); + controller = new FormDraftController(); + // Use reflection to inject the mock service since there's no public setter + java.lang.reflect.Field field = controller.getClass().getDeclaredField("formDraftService"); + field.setAccessible(true); + field.set(controller, formDraftService); + } + + @Test + public void getDraft_shouldReturnEmptyResponseWhenNoDraftExists() { + when(formDraftService.getDraft(PATIENT_UUID, PROVIDER_UUID)).thenReturn(null); + + ResponseEntity response = controller.getDraft(PATIENT_UUID, PROVIDER_UUID); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.getBody() instanceof FormDraftResponse); + } + + @Test + public void getDraft_shouldReturnBadRequestWhenServiceThrowsException() { + doThrow(new IllegalArgumentException("Invalid UUID")).when(formDraftService).getDraft(PATIENT_UUID, PROVIDER_UUID); + + ResponseEntity response = controller.getDraft(PATIENT_UUID, PROVIDER_UUID); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + } + + @Test + public void saveDraft_shouldReturnBadRequestWhenValidationFails() { + FormDraftRequest request = buildFormDraftRequest(null, PROVIDER_UUID, null, "{\"form\":\"data\"}"); + doThrow(new IllegalArgumentException("Patient UUID is required")).when(formDraftService).saveDraft(any(FormDraftRequest.class)); + + ResponseEntity response = controller.saveDraft(request); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + } + + @Test + public void saveDraft_shouldReturnBadRequestWhenServiceThrowsException() { + FormDraftRequest request = buildFormDraftRequest(PATIENT_UUID, PROVIDER_UUID, null, "{\"form\":\"data\"}"); + doThrow(new RuntimeException("Unexpected error")).when(formDraftService).saveDraft(any(FormDraftRequest.class)); + + ResponseEntity response = controller.saveDraft(request); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + } + + @Test + public void markDraftAsSaved_shouldReturnBadRequestWhenPatientUuidIsNull() { + doThrow(new IllegalArgumentException("Patient UUID is required")).when(formDraftService) + .markDraftAsSaved(null, PROVIDER_UUID); + + ResponseEntity response = controller.markDraftAsSaved(null, PROVIDER_UUID); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + } + + @Test + public void markDraftAsSaved_shouldReturnBadRequestWhenProviderUuidIsEmpty() { + doThrow(new IllegalArgumentException("Provider UUID is required")).when(formDraftService) + .markDraftAsSaved(PATIENT_UUID, ""); + + ResponseEntity response = controller.markDraftAsSaved(PATIENT_UUID, ""); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + } + + @Test + public void markDraftAsSaved_shouldReturnBadRequestWhenServiceThrows() { + doThrow(new RuntimeException("Service error")).when(formDraftService) + .markDraftAsSaved(PATIENT_UUID, PROVIDER_UUID); + + ResponseEntity response = controller.markDraftAsSaved(PATIENT_UUID, PROVIDER_UUID); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + } + + + // --- Helpers --- + + private FormDraftRequest buildFormDraftRequest(String patientUuid, String providerUuid, String encounterUuid, String formData) { + FormDraftRequest request = new FormDraftRequest(); + request.setPatientUuid(patientUuid); + request.setProviderUuid(providerUuid); + request.setEncounterUuid(encounterUuid); + request.setFormData(formData); + return request; + } + + private FormDraft buildFormDraft(String uuid, String formDataPath) { + FormDraft draft = new FormDraft(); + draft.setUuid(uuid); + draft.setFormDataPath(formDataPath); + draft.setDateCreated(new Date()); + return draft; + } +} From 127f38204f03dc03f970ef5d11db2b03f82631d8 Mon Sep 17 00:00:00 2001 From: Pooja Sastry Date: Mon, 20 Apr 2026 18:25:57 +0530 Subject: [PATCH 05/13] Pooja | Hive-119020 | Remove commenrs --- .../service/impl/FormDraftServiceImpl.java | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java index 2b0b410f12..20ca5897b6 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java @@ -1,10 +1,18 @@ package org.bahmni.module.bahmnicore.service.impl; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Date; +import java.util.UUID; + import org.bahmni.module.bahmnicore.contract.FormDraftRequest; import org.bahmni.module.bahmnicore.dao.FormDraftDAO; import org.bahmni.module.bahmnicore.model.FormDraft; import org.bahmni.module.bahmnicore.service.FormDraftService; -import org.openmrs.api.context.Context; import org.openmrs.Encounter; import org.openmrs.Patient; import org.openmrs.User; @@ -12,20 +20,12 @@ import org.openmrs.api.EncounterService; import org.openmrs.api.PatientService; import org.openmrs.api.UserService; +import org.openmrs.api.context.Context; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.Date; -import java.util.UUID; - @Transactional public class FormDraftServiceImpl implements FormDraftService { @@ -36,13 +36,11 @@ public class FormDraftServiceImpl implements FormDraftService { private PatientService patientService; private UserService userService; private EncounterService encounterService; - private User authenticatedUser; // For testing - overrides Context.getAuthenticatedUser() + private User authenticatedUser; - // For testing purposes - can be overridden private String formDraftsBasePath; public FormDraftServiceImpl() { - // Initialize with OPENMRS_APPLICATION_DATA_DIRECTORY String appDataDir = System.getProperty("OPENMRS_APPLICATION_DATA_DIRECTORY"); if (appDataDir == null || appDataDir.isEmpty()) { throw new IllegalStateException("OPENMRS_APPLICATION_DATA_DIRECTORY system property not set"); @@ -70,7 +68,6 @@ public void setEncounterService(EncounterService encounterService) { this.encounterService = encounterService; } - // Package-private setters for testing protected void setFormDraftsBasePath(String basePath) { this.formDraftsBasePath = basePath; } @@ -104,7 +101,6 @@ public FormDraft saveDraft(FormDraftRequest request) { boolean isNewDraft = (draft == null); boolean contentChanged = true; - // If the latest draft is marked as saved, create a new draft instead of updating if (draft != null && draft.getMarkedAsSaved() != null && draft.getMarkedAsSaved()) { isNewDraft = true; draft = null; @@ -170,14 +166,14 @@ private boolean hasFormDataChanged(String filePath, String newFormData) { try { File file = new File(filePath); if (!file.exists()) { - return true; // File doesn't exist, so content is new + return true; } String existingContent = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); return !existingContent.equals(newFormData); } catch (IOException e) { log.warn("Error reading existing form data file, assuming content changed", e); - return true; // If we can't read, assume it changed to be safe + return true; } } @@ -244,7 +240,6 @@ private void writeFormDataToFile(String filePath, String formData) throws IOExce @Override public FormDraft getDraft(String patientUuid, String providerUuid) { try { - // Validate required fields if (patientUuid == null || patientUuid.isEmpty()) { throw new IllegalArgumentException("Patient UUID is required"); } @@ -252,7 +247,6 @@ public FormDraft getDraft(String patientUuid, String providerUuid) { throw new IllegalArgumentException("Provider UUID is required"); } - // Fetch entities to get their IDs PatientService ps = patientService != null ? patientService : Context.getPatientService(); Patient patient = ps.getPatientByUuid(patientUuid); if (patient == null) { @@ -265,7 +259,6 @@ public FormDraft getDraft(String patientUuid, String providerUuid) { return null; } - // Query by patient ID and user ID return formDraftDAO.getLatestByPatientAndUser(patient.getPatientId(), user.getUserId()); } catch (IllegalArgumentException e) { @@ -279,7 +272,6 @@ public FormDraft getDraft(String patientUuid, String providerUuid) { @Override public void discardDraft(String patientUuid, String providerUuid) { try { - // Validate required fields if (patientUuid == null || patientUuid.isEmpty()) { throw new IllegalArgumentException("Patient UUID is required"); } @@ -287,7 +279,6 @@ public void discardDraft(String patientUuid, String providerUuid) { throw new IllegalArgumentException("Provider UUID is required"); } - // Fetch entities to get their IDs PatientService ps = patientService != null ? patientService : Context.getPatientService(); Patient patient = ps.getPatientByUuid(patientUuid); if (patient == null) { @@ -300,7 +291,6 @@ public void discardDraft(String patientUuid, String providerUuid) { throw new APIException("User/Provider not found with UUID: " + providerUuid); } - // Delete (void) latest draft for this patient-provider pair formDraftDAO.deleteLatestDraft(patient.getPatientId(), user.getUserId()); } catch (IllegalArgumentException e) { @@ -334,7 +324,6 @@ public String getFormData(String formDataPath) { @Override public void markDraftAsSaved(String patientUuid, String providerUuid) { try { - // Validate required fields if (patientUuid == null || patientUuid.isEmpty()) { throw new IllegalArgumentException("Patient UUID is required"); } @@ -342,7 +331,6 @@ public void markDraftAsSaved(String patientUuid, String providerUuid) { throw new IllegalArgumentException("Provider UUID is required"); } - // Fetch entities to get their IDs PatientService ps = patientService != null ? patientService : Context.getPatientService(); Patient patient = ps.getPatientByUuid(patientUuid); if (patient == null) { @@ -355,7 +343,6 @@ public void markDraftAsSaved(String patientUuid, String providerUuid) { throw new APIException("User/Provider not found with UUID: " + providerUuid); } - // Get latest draft and mark as saved FormDraft draft = formDraftDAO.getLatestByPatientAndUser(patient.getPatientId(), user.getUserId()); if (draft != null) { draft.setMarkedAsSaved(true); From 2c4b2fe41fe6393a7e2da0da75ebb5371ff8c4e9 Mon Sep 17 00:00:00 2001 From: Pooja Sastry Date: Fri, 24 Apr 2026 15:38:51 +0530 Subject: [PATCH 06/13] Pooja | Bug-113307 | Manual Save as Draft Issues (#30) * Pooja | Bug-113307 | Manual Save as Draft Issues * Update github validate PR workflow setup java version --------- Co-authored-by: SasikiranJ --- .github/workflows/validate_pr.yml | 5 +- .../service/impl/FormDraftServiceImpl.java | 37 ++++++++--- .../impl/FormDraftServiceImplTest.java | 62 +++++++++++++++---- 3 files changed, 81 insertions(+), 23 deletions(-) diff --git a/.github/workflows/validate_pr.yml b/.github/workflows/validate_pr.yml index 336fb5e46c..d4a8664342 100644 --- a/.github/workflows/validate_pr.yml +++ b/.github/workflows/validate_pr.yml @@ -14,9 +14,10 @@ jobs: with: fetch-depth: 0 - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 1.8 + java-version: 8 + distribution: 'zulu' - name: Cache Maven packages uses: actions/cache@v3 with: diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java index 20ca5897b6..1e7de94047 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java @@ -6,6 +6,7 @@ import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.Collection; import java.util.Date; import java.util.UUID; @@ -15,10 +16,12 @@ import org.bahmni.module.bahmnicore.service.FormDraftService; import org.openmrs.Encounter; import org.openmrs.Patient; +import org.openmrs.Provider; import org.openmrs.User; import org.openmrs.api.APIException; import org.openmrs.api.EncounterService; import org.openmrs.api.PatientService; +import org.openmrs.api.ProviderService; import org.openmrs.api.UserService; import org.openmrs.api.context.Context; import org.slf4j.Logger; @@ -35,6 +38,7 @@ public class FormDraftServiceImpl implements FormDraftService { private FormDraftDAO formDraftDAO; private PatientService patientService; private UserService userService; + private ProviderService providerService; private EncounterService encounterService; private User authenticatedUser; @@ -63,6 +67,11 @@ public void setUserService(UserService userService) { this.userService = userService; } + @Autowired(required = false) + public void setProviderService(ProviderService providerService) { + this.providerService = providerService; + } + @Autowired(required = false) public void setEncounterService(EncounterService encounterService) { this.encounterService = encounterService; @@ -80,6 +89,22 @@ private User getAuthenticatedUser() { return authenticatedUser != null ? authenticatedUser : Context.getAuthenticatedUser(); } + /** + * Resolves a User from a Provider UUID via Provider → Person → User lookup. + */ + private User resolveUser(String providerUuid) { + ProviderService ps = this.providerService != null ? this.providerService : Context.getProviderService(); + Provider provider = ps.getProviderByUuid(providerUuid); + if (provider != null && provider.getPerson() != null) { + UserService us = userService != null ? userService : Context.getUserService(); + Collection users = us.getUsersByPerson(provider.getPerson(), false); + if (users != null && !users.isEmpty()) { + return users.iterator().next(); + } + } + return null; + } + @Override public FormDraft saveDraft(FormDraftRequest request) { try { @@ -91,8 +116,7 @@ public FormDraft saveDraft(FormDraftRequest request) { throw new APIException("Patient not found with UUID: " + request.getPatientUuid()); } - UserService us = userService != null ? userService : Context.getUserService(); - User user = us.getUserByUuid(request.getProviderUuid()); + User user = resolveUser(request.getProviderUuid()); if (user == null) { throw new APIException("User/Provider not found with UUID: " + request.getProviderUuid()); } @@ -253,8 +277,7 @@ public FormDraft getDraft(String patientUuid, String providerUuid) { return null; } - UserService us = userService != null ? userService : Context.getUserService(); - User user = us.getUserByUuid(providerUuid); + User user = resolveUser(providerUuid); if (user == null) { return null; } @@ -285,8 +308,7 @@ public void discardDraft(String patientUuid, String providerUuid) { throw new APIException("Patient not found with UUID: " + patientUuid); } - UserService us = userService != null ? userService : Context.getUserService(); - User user = us.getUserByUuid(providerUuid); + User user = resolveUser(providerUuid); if (user == null) { throw new APIException("User/Provider not found with UUID: " + providerUuid); } @@ -337,8 +359,7 @@ public void markDraftAsSaved(String patientUuid, String providerUuid) { throw new APIException("Patient not found with UUID: " + patientUuid); } - UserService us = userService != null ? userService : Context.getUserService(); - User user = us.getUserByUuid(providerUuid); + User user = resolveUser(providerUuid); if (user == null) { throw new APIException("User/Provider not found with UUID: " + providerUuid); } diff --git a/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java b/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java index 916e893ff7..8c67e9e20c 100644 --- a/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java +++ b/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java @@ -1,5 +1,7 @@ package org.bahmni.module.bahmnicore.service.impl; +import java.util.Collections; + import org.bahmni.module.bahmnicore.contract.FormDraftRequest; import org.bahmni.module.bahmnicore.dao.FormDraftDAO; import org.bahmni.module.bahmnicore.model.FormDraft; @@ -13,10 +15,13 @@ import org.mockito.MockitoAnnotations; import org.openmrs.Encounter; import org.openmrs.Patient; +import org.openmrs.Person; +import org.openmrs.Provider; import org.openmrs.User; import org.openmrs.api.APIException; import org.openmrs.api.EncounterService; import org.openmrs.api.PatientService; +import org.openmrs.api.ProviderService; import org.openmrs.api.UserService; import static org.junit.Assert.assertEquals; @@ -43,10 +48,14 @@ public class FormDraftServiceImplTest { @Mock private UserService userService; + @Mock + private ProviderService providerService; + @Mock private EncounterService encounterService; private FormDraftServiceImpl formDraftService; + private Person person; private static final String PATIENT_UUID = "patient-uuid-123"; private static final int PATIENT_ID = 1; @@ -65,12 +74,15 @@ public void setUp() throws Exception { formDraftService.setFormDraftDAO(formDraftDAO); formDraftService.setPatientService(patientService); formDraftService.setUserService(userService); + formDraftService.setProviderService(providerService); formDraftService.setEncounterService(encounterService); // Set authenticated user for testing User mockUser = new User(); mockUser.setUuid("user-uuid"); formDraftService.setAuthenticatedUser(mockUser); + + person = new Person(); } @After @@ -86,7 +98,7 @@ public void saveDraft_shouldCreateNewDraftWhenNoneExists() { User user = buildUser(PROVIDER_UUID, PROVIDER_ID); when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + mockProviderResolution(user); when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); @@ -114,7 +126,7 @@ public void saveDraft_shouldUpdateExistingDraftForSamePatientProvider() { existingDraft.setUser(user); when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + mockProviderResolution(user); when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(existingDraft); when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); @@ -134,7 +146,7 @@ public void saveDraft_shouldSetEncounterWhenEncounterUuidIsProvided() { encounter.setUuid(ENCOUNTER_UUID); when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + mockProviderResolution(user); when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); when(encounterService.getEncounterByUuid(ENCOUNTER_UUID)).thenReturn(encounter); when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); @@ -151,7 +163,7 @@ public void saveDraft_shouldNotFailWhenEncounterUuidNotFound() { User user = buildUser(PROVIDER_UUID, PROVIDER_ID); when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + mockProviderResolution(user); when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); when(encounterService.getEncounterByUuid("nonexistent-encounter")).thenReturn(null); when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); @@ -186,7 +198,7 @@ public void saveDraft_shouldPersistFormDataPath() { User user = buildUser(PROVIDER_UUID, PROVIDER_ID); when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + mockProviderResolution(user); when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); ArgumentCaptor captor = ArgumentCaptor.forClass(FormDraft.class); @@ -210,7 +222,7 @@ public void getDraft_shouldReturnDraftForValidPatientAndProvider() { User user = buildUser(PROVIDER_UUID, PROVIDER_ID); when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + mockProviderResolution(user); when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(existingDraft); FormDraft result = formDraftService.getDraft(PATIENT_UUID, PROVIDER_UUID); @@ -225,7 +237,7 @@ public void getDraft_shouldReturnNullWhenNoDraftExists() { User user = buildUser(PROVIDER_UUID, PROVIDER_ID); when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + mockProviderResolution(user); when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); FormDraft result = formDraftService.getDraft(PATIENT_UUID, PROVIDER_UUID); @@ -249,7 +261,7 @@ public void discardDraft_shouldCallDaoDeleteLatestDraft() { User user = buildUser(PROVIDER_UUID, PROVIDER_ID); when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + mockProviderResolution(user); formDraftService.discardDraft(PATIENT_UUID, PROVIDER_UUID); @@ -276,7 +288,7 @@ public void markDraftAsSaved_shouldUpdateDraftMarkedAsSavedFlag() { User user = buildUser(PROVIDER_UUID, PROVIDER_ID); when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + mockProviderResolution(user); when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(existingDraft); when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); @@ -296,7 +308,7 @@ public void markDraftAsSaved_shouldDoNothingWhenNoDraftExists() { User user = buildUser(PROVIDER_UUID, PROVIDER_ID); when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + mockProviderResolution(user); when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); formDraftService.markDraftAsSaved(PATIENT_UUID, PROVIDER_UUID); @@ -336,11 +348,28 @@ public void markDraftAsSaved_shouldThrowWhenProviderNotFound() { Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(null); + when(providerService.getProviderByUuid(PROVIDER_UUID)).thenReturn(null); formDraftService.markDraftAsSaved(PATIENT_UUID, PROVIDER_UUID); } + @Test + public void saveDraft_shouldResolveUserViaProvider() { + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null, "{\"form\":\"data\"}"); + Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); + User user = buildUser("user-uuid-999", PROVIDER_ID); + + mockProviderResolution(user); + when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); + when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); + when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); + + FormDraft result = formDraftService.saveDraft(request); + + assertNotNull(result); + assertEquals(user, result.getUser()); + } + @Test public void saveDraft_shouldCreateNewDraftWhenExistingDraftIsMarkedAsSaved() { FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null, "{\"updated\":\"data\"}"); @@ -354,7 +383,7 @@ public void saveDraft_shouldCreateNewDraftWhenExistingDraftIsMarkedAsSaved() { markedDraft.setMarkedAsSaved(true); when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + mockProviderResolution(user); when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(markedDraft); when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); @@ -374,7 +403,7 @@ public void saveDraft_shouldInitializeMarkedAsSavedAsFalseForNewDraft() { User user = buildUser(PROVIDER_UUID, PROVIDER_ID); when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - when(userService.getUserByUuid(PROVIDER_UUID)).thenReturn(user); + mockProviderResolution(user); when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); ArgumentCaptor captor = ArgumentCaptor.forClass(FormDraft.class); @@ -388,6 +417,13 @@ public void saveDraft_shouldInitializeMarkedAsSavedAsFalseForNewDraft() { // --- Helpers --- + private void mockProviderResolution(User user) { + Provider provider = new Provider(); + provider.setPerson(person); + when(providerService.getProviderByUuid(PROVIDER_UUID)).thenReturn(provider); + when(userService.getUsersByPerson(person, false)).thenReturn(Collections.singletonList(user)); + } + private FormDraftRequest buildRequest(String patientUuid, String providerUuid, String encounterUuid, String formData) { FormDraftRequest request = new FormDraftRequest(); request.setPatientUuid(patientUuid); From 146ccb58b636f1535c269e5b2503b7c8427ad665 Mon Sep 17 00:00:00 2001 From: sasikiran-tw Date: Mon, 18 May 2026 10:26:27 +0530 Subject: [PATCH 07/13] Implement an api to get the draft forms for a specific provider and to display in draft overlay (#31) --- .../contract/FormDraftSummaryResponse.java | 88 ++++++++++ .../module/bahmnicore/dao/FormDraftDAO.java | 11 ++ .../bahmnicore/dao/impl/FormDraftDaoImpl.java | 17 ++ .../bahmnicore/service/FormDraftService.java | 13 ++ .../service/impl/FormDraftServiceImpl.java | 84 +++++++++ .../impl/FormDraftServiceImplTest.java | 164 ++++++++++++++++++ .../v1_0/controller/FormDraftController.java | 23 +++ .../controller/FormDraftControllerTest.java | 44 +++++ 8 files changed, 444 insertions(+) create mode 100644 bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftSummaryResponse.java diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftSummaryResponse.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftSummaryResponse.java new file mode 100644 index 0000000000..e061b2bde9 --- /dev/null +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftSummaryResponse.java @@ -0,0 +1,88 @@ +package org.bahmni.module.bahmnicore.contract; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FormDraftSummaryResponse { + + @JsonProperty + private String draftUuid; + + @JsonProperty + private String patientUuid; + + @JsonProperty + private String patientName; + + @JsonProperty + private String patientIdentifier; + + @JsonProperty + private String encounterUuid; + + @JsonProperty + private String formName; + + @JsonProperty + private Long timestamp; + + public FormDraftSummaryResponse() { + } + + public String getDraftUuid() { + return draftUuid; + } + + public void setDraftUuid(String draftUuid) { + this.draftUuid = draftUuid; + } + + public String getPatientUuid() { + return patientUuid; + } + + public void setPatientUuid(String patientUuid) { + this.patientUuid = patientUuid; + } + + public String getPatientName() { + return patientName; + } + + public void setPatientName(String patientName) { + this.patientName = patientName; + } + + public String getPatientIdentifier() { + return patientIdentifier; + } + + public void setPatientIdentifier(String patientIdentifier) { + this.patientIdentifier = patientIdentifier; + } + + public String getEncounterUuid() { + return encounterUuid; + } + + public void setEncounterUuid(String encounterUuid) { + this.encounterUuid = encounterUuid; + } + + public String getFormName() { + return formName; + } + + public void setFormName(String formName) { + this.formName = formName; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } +} diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java index 2aea115f5b..93d84adb8f 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java @@ -2,6 +2,8 @@ import org.bahmni.module.bahmnicore.model.FormDraft; +import java.util.List; + public interface FormDraftDAO { /** @@ -30,4 +32,13 @@ public interface FormDraftDAO { * @param userId the OpenMRS user ID (provider) */ void deleteLatestDraft(Integer patientId, Integer userId); + + /** + * Retrieve all non-voided, unsaved drafts for a user, ordered newest first. + * Drafts where markedAsSaved is true are excluded. + * + * @param userId the OpenMRS user ID (provider) + * @return list of FormDraft objects, ordered by COALESCE(dateChanged, dateCreated) DESC + */ + List getAllByUserOrderedByDateDesc(Integer userId); } diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java index 8826b3e674..d779d6e3ba 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import java.util.Date; +import java.util.List; public class FormDraftDaoImpl implements FormDraftDAO { @@ -66,4 +67,20 @@ public void deleteLatestDraft(Integer patientId, Integer userId) throws DAOExcep throw new DAOException("Failed to delete form draft", e); } } + + @Override + public List getAllByUserOrderedByDateDesc(Integer userId) throws DAOException { + try { + Query query = sessionFactory.getCurrentSession() + .createQuery("FROM FormDraft WHERE user.userId = :userId " + + "AND voided = false " + + "AND (markedAsSaved IS NULL OR markedAsSaved = false) " + + "ORDER BY COALESCE(dateChanged, dateCreated) DESC", FormDraft.class); + query.setParameter("userId", userId); + return query.getResultList(); + } catch (Exception e) { + log.error("Error retrieving all form drafts for user: " + userId, e); + throw new DAOException("Failed to retrieve form drafts for user", e); + } + } } diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java index e914d2577a..474abc76b4 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java @@ -1,8 +1,11 @@ package org.bahmni.module.bahmnicore.service; import org.bahmni.module.bahmnicore.contract.FormDraftRequest; +import org.bahmni.module.bahmnicore.contract.FormDraftSummaryResponse; import org.bahmni.module.bahmnicore.model.FormDraft; +import java.util.List; + public interface FormDraftService { /** @@ -47,4 +50,14 @@ public interface FormDraftService { * @param providerUuid the UUID of the provider */ void markDraftAsSaved(String patientUuid, String providerUuid); + + /** + * Retrieve a summary list of all unsaved drafts for a given provider. + * Reads formData to extract formUuid/formName where available. + * Drafts with missing patient name or identifier are skipped with a warning log. + * + * @param providerUuid the UUID of the provider + * @return list of FormDraftSummaryResponse, ordered newest first; empty list if provider not found + */ + List getDraftsByProvider(String providerUuid); } diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java index 1e7de94047..273d41d917 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java @@ -6,11 +6,16 @@ import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.ArrayList; import java.util.Collection; import java.util.Date; +import java.util.List; import java.util.UUID; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.bahmni.module.bahmnicore.contract.FormDraftRequest; +import org.bahmni.module.bahmnicore.contract.FormDraftSummaryResponse; import org.bahmni.module.bahmnicore.dao.FormDraftDAO; import org.bahmni.module.bahmnicore.model.FormDraft; import org.bahmni.module.bahmnicore.service.FormDraftService; @@ -34,6 +39,7 @@ public class FormDraftServiceImpl implements FormDraftService { private static final Logger log = LoggerFactory.getLogger(FormDraftServiceImpl.class); private static final String FORM_DRAFTS_SUBDIRECTORY = "form_draft"; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private FormDraftDAO formDraftDAO; private PatientService patientService; @@ -343,6 +349,84 @@ public String getFormData(String formDataPath) { } } + @Override + public List getDraftsByProvider(String providerUuid) { + if (providerUuid == null || providerUuid.trim().isEmpty()) { + throw new IllegalArgumentException("Provider UUID is required"); + } + + User user = resolveUser(providerUuid); + if (user == null) { + log.warn("getDraftsByProvider: no user found for providerUuid={}", providerUuid); + return new ArrayList<>(); + } + + List drafts = formDraftDAO.getAllByUserOrderedByDateDesc(user.getUserId()); + List results = new ArrayList<>(); + for (FormDraft draft : drafts) { + FormDraftSummaryResponse summary = buildSummary(draft); + if (summary != null) { + results.add(summary); + } + } + return results; + } + + private FormDraftSummaryResponse buildSummary(FormDraft draft) { + Patient patient = draft.getPatient(); + if (patient == null) { + log.warn("buildSummary: draft {} has null patient — skipping", draft.getUuid()); + return null; + } + + String patientName = patient.getPersonName() != null + ? patient.getPersonName().getFullName() + : ""; + String patientIdentifier = patient.getPatientIdentifier() != null + ? patient.getPatientIdentifier().getIdentifier() + : null; + String encounterUuid = draft.getEncounter() != null ? draft.getEncounter().getUuid() : null; + long timestamp = draft.getDateChanged() != null + ? draft.getDateChanged().getTime() + : draft.getDateCreated().getTime(); + + String formName = extractFormName(draft.getFormDataPath()); + + FormDraftSummaryResponse response = new FormDraftSummaryResponse(); + response.setDraftUuid(draft.getUuid()); + response.setPatientUuid(patient.getUuid()); + response.setPatientName(patientName); + response.setPatientIdentifier(patientIdentifier); + response.setEncounterUuid(encounterUuid); + response.setFormName(formName); + response.setTimestamp(timestamp); + return response; + } + + private String extractFormName(String formDataPath) { + String formData = getFormData(formDataPath); + if (formData == null || formData.trim().isEmpty()) { + return null; + } + try { + JsonNode root = OBJECT_MAPPER.readTree(formData); + if (!root.isArray()) { + log.warn("extractFormName: expected observations array but got object at path={}", formDataPath); + return null; + } + for (JsonNode obs : root) { + String formFieldPath = obs.path("formFieldPath").asText(null); + if (formFieldPath != null && !formFieldPath.isEmpty()) { + return formFieldPath.split("\\.")[0]; + } + } + return null; + } catch (Exception e) { + log.warn("extractFormName: failed to parse form data at path={}", formDataPath, e); + return null; + } + } + @Override public void markDraftAsSaved(String patientUuid, String providerUuid) { try { diff --git a/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java b/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java index 8c67e9e20c..3954498168 100644 --- a/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java +++ b/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java @@ -1,8 +1,15 @@ package org.bahmni.module.bahmnicore.service.impl; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collections; +import java.util.List; import org.bahmni.module.bahmnicore.contract.FormDraftRequest; +import org.bahmni.module.bahmnicore.contract.FormDraftSummaryResponse; import org.bahmni.module.bahmnicore.dao.FormDraftDAO; import org.bahmni.module.bahmnicore.model.FormDraft; import org.junit.After; @@ -15,7 +22,9 @@ import org.mockito.MockitoAnnotations; import org.openmrs.Encounter; import org.openmrs.Patient; +import org.openmrs.PatientIdentifier; import org.openmrs.Person; +import org.openmrs.PersonName; import org.openmrs.Provider; import org.openmrs.User; import org.openmrs.api.APIException; @@ -415,6 +424,143 @@ public void saveDraft_shouldInitializeMarkedAsSavedAsFalseForNewDraft() { assertFalse(saved.getMarkedAsSaved()); } + + @Test + public void getDraftsByProvider_returnsDraftsNewestFirst() throws Exception { + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + mockProviderResolution(user); + + Patient patient = buildPatientWithDetails(PATIENT_UUID, PATIENT_ID, "John Doe", "ET001"); + Encounter encounter = new Encounter(); + encounter.setUuid(ENCOUNTER_UUID); + + // formData is a serialized observations array; formName is derived from formFieldPath prefix + File formDataFile = temporaryFolder.newFile("draft-form-identity.json"); + writeFile(formDataFile, "[{\"formFieldPath\":\"Vitals.1/1-0\",\"concept\":{\"name\":\"Weight\"},\"value\":70}]"); + + FormDraft draftOlder = new FormDraft(); + draftOlder.setUuid("draft-uuid-older"); + draftOlder.setPatient(patient); + draftOlder.setEncounter(encounter); + draftOlder.setFormDataPath(formDataFile.getAbsolutePath()); + draftOlder.setDateCreated(new java.util.Date(1000L)); + + FormDraft draftNewer = new FormDraft(); + draftNewer.setUuid("draft-uuid-newer"); + draftNewer.setPatient(patient); + draftNewer.setEncounter(null); + draftNewer.setFormDataPath(formDataFile.getAbsolutePath()); + draftNewer.setDateCreated(new java.util.Date(2000L)); + draftNewer.setDateChanged(new java.util.Date(3000L)); + + when(formDraftDAO.getAllByUserOrderedByDateDesc(PROVIDER_ID)).thenReturn(Arrays.asList(draftNewer, draftOlder)); + + List results = formDraftService.getDraftsByProvider(PROVIDER_UUID); + + assertEquals(2, results.size()); + assertEquals("draft-uuid-newer", results.get(0).getDraftUuid()); + assertEquals(3000L, (long) results.get(0).getTimestamp()); + assertNull(results.get(0).getEncounterUuid()); + assertEquals("Vitals", results.get(0).getFormName()); + assertEquals("draft-uuid-older", results.get(1).getDraftUuid()); + assertEquals(1000L, (long) results.get(1).getTimestamp()); + assertEquals(ENCOUNTER_UUID, results.get(1).getEncounterUuid()); + } + + @Test + public void getDraftsByProvider_returnsEmptyList_whenNoDrafts() { + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + mockProviderResolution(user); + when(formDraftDAO.getAllByUserOrderedByDateDesc(PROVIDER_ID)).thenReturn(Collections.emptyList()); + + List results = formDraftService.getDraftsByProvider(PROVIDER_UUID); + + assertNotNull(results); + assertTrue(results.isEmpty()); + } + + @Test + public void getDraftsByProvider_returnsEmptyList_whenProviderNotFound() { + when(providerService.getProviderByUuid(PROVIDER_UUID)).thenReturn(null); + + List results = formDraftService.getDraftsByProvider(PROVIDER_UUID); + + assertNotNull(results); + assertTrue(results.isEmpty()); + } + + @Test(expected = IllegalArgumentException.class) + public void getDraftsByProvider_throwsWhenProviderUuidIsNull() { + formDraftService.getDraftsByProvider(null); + } + + @Test(expected = IllegalArgumentException.class) + public void getDraftsByProvider_throwsWhenProviderUuidIsBlank() { + formDraftService.getDraftsByProvider(" "); + } + + @Test + public void getDraftsByProvider_skipsEntry_whenPatientIsNull() { + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + mockProviderResolution(user); + + FormDraft draftWithNullPatient = new FormDraft(); + draftWithNullPatient.setUuid("draft-no-patient"); + draftWithNullPatient.setPatient(null); + draftWithNullPatient.setDateCreated(new java.util.Date()); + + when(formDraftDAO.getAllByUserOrderedByDateDesc(PROVIDER_ID)).thenReturn(Collections.singletonList(draftWithNullPatient)); + + List results = formDraftService.getDraftsByProvider(PROVIDER_UUID); + + assertTrue(results.isEmpty()); + } + + @Test + public void getDraftsByProvider_setsNullFormFields_whenFormDataIsMalformedJson() throws Exception { + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + mockProviderResolution(user); + + Patient patient = buildPatientWithDetails(PATIENT_UUID, PATIENT_ID, "Jane Smith", "ET002"); + + File malformedFile = temporaryFolder.newFile("malformed-draft.json"); + writeFile(malformedFile, "NOT_VALID_JSON{{{{"); + + FormDraft draft = new FormDraft(); + draft.setUuid("draft-malformed"); + draft.setPatient(patient); + draft.setFormDataPath(malformedFile.getAbsolutePath()); + draft.setDateCreated(new java.util.Date()); + + when(formDraftDAO.getAllByUserOrderedByDateDesc(PROVIDER_ID)).thenReturn(Collections.singletonList(draft)); + + List results = formDraftService.getDraftsByProvider(PROVIDER_UUID); + + assertEquals(1, results.size()); + assertNull(results.get(0).getFormName()); + } + + @Test + public void getDraftsByProvider_setsNullFormFields_whenFormDataFileAbsent() { + User user = buildUser(PROVIDER_UUID, PROVIDER_ID); + mockProviderResolution(user); + + Patient patient = buildPatientWithDetails(PATIENT_UUID, PATIENT_ID, "Bob Jones", "ET003"); + + FormDraft draft = new FormDraft(); + draft.setUuid("draft-no-file"); + draft.setPatient(patient); + draft.setFormDataPath("/nonexistent/path/draft.json"); + draft.setDateCreated(new java.util.Date()); + + when(formDraftDAO.getAllByUserOrderedByDateDesc(PROVIDER_ID)).thenReturn(Collections.singletonList(draft)); + + List results = formDraftService.getDraftsByProvider(PROVIDER_UUID); + + assertEquals(1, results.size()); + assertNull(results.get(0).getFormName()); + } + // --- Helpers --- private void mockProviderResolution(User user) { @@ -446,4 +592,22 @@ private User buildUser(String uuid, int userId) { user.setUserId(userId); return user; } + + private Patient buildPatientWithDetails(String uuid, int patientId, String fullName, String identifier) { + Patient patient = buildPatient(uuid, patientId); + PersonName personName = new PersonName(); + personName.setGivenName(fullName.split(" ")[0]); + personName.setFamilyName(fullName.contains(" ") ? fullName.split(" ")[1] : ""); + patient.addName(personName); + PatientIdentifier patientIdentifier = new PatientIdentifier(); + patientIdentifier.setIdentifier(identifier); + patient.addIdentifier(patientIdentifier); + return patient; + } + + private void writeFile(File file, String content) throws Exception { + try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) { + writer.write(content); + } + } } diff --git a/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java b/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java index 386326ffe0..2bd05e214c 100644 --- a/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java +++ b/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java @@ -2,6 +2,7 @@ import org.bahmni.module.bahmnicore.contract.FormDraftRequest; import org.bahmni.module.bahmnicore.contract.FormDraftResponse; +import org.bahmni.module.bahmnicore.contract.FormDraftSummaryResponse; import org.bahmni.module.bahmnicore.model.FormDraft; import org.bahmni.module.bahmnicore.security.PrivilegeConstants; import org.bahmni.module.bahmnicore.service.FormDraftService; @@ -21,6 +22,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; +import java.util.List; + @Controller @RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/bahmnicore/formdraft") public class FormDraftController extends BaseRestController { @@ -30,6 +33,26 @@ public class FormDraftController extends BaseRestController { @Autowired private FormDraftService formDraftService; + /** + * List all unsaved drafts for a given provider. + * GET /rest/v1/bahmnicore/formdraft/list?providerUuid=xxx + */ + @RequestMapping(value = "/list", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity getDraftsByProvider( + @RequestParam(value = "providerUuid", required = true) String providerUuid) { + try { + List drafts = formDraftService.getDraftsByProvider(providerUuid); + return new ResponseEntity<>(drafts, HttpStatus.OK); + } catch (IllegalArgumentException e) { + log.warn("Invalid request for draft list", e); + return new ResponseEntity<>(WebUtils.wrapErrorResponse(null, e.getMessage()), HttpStatus.BAD_REQUEST); + } catch (Exception e) { + log.error("Error retrieving draft list for provider: " + providerUuid, e); + return new ResponseEntity<>(WebUtils.wrapErrorResponse(null, e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); + } + } + /** * Auto-save a form draft. Upserts by patient and provider UUID. * POST /rest/v1/bahmnicore/formdraft diff --git a/bahmnicore-omod/src/test/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftControllerTest.java b/bahmnicore-omod/src/test/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftControllerTest.java index d610c3acb5..9a47f40015 100644 --- a/bahmnicore-omod/src/test/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftControllerTest.java +++ b/bahmnicore-omod/src/test/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftControllerTest.java @@ -2,6 +2,7 @@ import org.bahmni.module.bahmnicore.contract.FormDraftRequest; import org.bahmni.module.bahmnicore.contract.FormDraftResponse; +import org.bahmni.module.bahmnicore.contract.FormDraftSummaryResponse; import org.bahmni.module.bahmnicore.model.FormDraft; import org.bahmni.module.bahmnicore.service.FormDraftService; import org.junit.Before; @@ -9,7 +10,10 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; +import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -109,6 +113,46 @@ public void markDraftAsSaved_shouldReturnBadRequestWhenServiceThrows() { assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } + + @Test + public void getDraftsByProvider_returns200WithList() { + FormDraftSummaryResponse summary = new FormDraftSummaryResponse(); + summary.setDraftUuid("draft-uuid-1"); + summary.setPatientUuid("patient-uuid-1"); + summary.setPatientName("John Doe"); + summary.setPatientIdentifier("ET001"); + summary.setTimestamp(1000L); + when(formDraftService.getDraftsByProvider(PROVIDER_UUID)).thenReturn(Collections.singletonList(summary)); + + ResponseEntity response = controller.getDraftsByProvider(PROVIDER_UUID); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + List body = (List) response.getBody(); + assertNotNull(body); + assertEquals(1, body.size()); + } + + @Test + public void getDraftsByProvider_returns200WithEmptyList() { + when(formDraftService.getDraftsByProvider(PROVIDER_UUID)).thenReturn(Collections.emptyList()); + + ResponseEntity response = controller.getDraftsByProvider(PROVIDER_UUID); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + List body = (List) response.getBody(); + assertNotNull(body); + assertTrue(body.isEmpty()); + } + + @Test + public void getDraftsByProvider_returns400_whenProviderUuidIsInvalid() { + doThrow(new IllegalArgumentException("Provider UUID is required")).when(formDraftService) + .getDraftsByProvider(" "); + + ResponseEntity response = controller.getDraftsByProvider(" "); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + } // --- Helpers --- From 9e66f74b5033fc4c56d603f8d43b6238f7cc1d52 Mon Sep 17 00:00:00 2001 From: Pooja Sastry Date: Mon, 18 May 2026 14:55:26 +0530 Subject: [PATCH 08/13] Pooja | Bug-116474 | Fix for Issue #3 | Remove privilege requirement for discarding drafts (#32) --- .../bahmnicore/security/PrivilegeConstants.java | 1 - .../v1_0/controller/FormDraftController.java | 10 +--------- .../src/main/resources/liquibase.xml | 17 +++++++++-------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/security/PrivilegeConstants.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/security/PrivilegeConstants.java index 06619428fe..d81df6da1f 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/security/PrivilegeConstants.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/security/PrivilegeConstants.java @@ -3,5 +3,4 @@ public class PrivilegeConstants { public static final String DELETE_PATIENT_DOCUMENT_PRIVILEGE = "Delete Patient Document"; public static final String IMPORT_CSV_FILE_PRIVILEGE = "Import CSV Files"; - public static final String DELETE_FORM_DRAFT_PRIVILEGE = "Delete Form Draft"; } diff --git a/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java b/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java index 2bd05e214c..1417c61e1d 100644 --- a/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java +++ b/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftController.java @@ -4,7 +4,6 @@ import org.bahmni.module.bahmnicore.contract.FormDraftResponse; import org.bahmni.module.bahmnicore.contract.FormDraftSummaryResponse; import org.bahmni.module.bahmnicore.model.FormDraft; -import org.bahmni.module.bahmnicore.security.PrivilegeConstants; import org.bahmni.module.bahmnicore.service.FormDraftService; import org.bahmni.module.bahmnicore.util.WebUtils; import org.openmrs.api.context.Context; @@ -154,20 +153,13 @@ public ResponseEntity markDraftAsSaved( * * @param patientUuid the UUID of the patient * @param providerUuid the UUID of the provider - * @return 204 No Content on success, 403 Forbidden if insufficient privileges + * @return 204 No Content on success */ @RequestMapping(method = RequestMethod.DELETE) @ResponseBody public ResponseEntity discardDraft( @RequestParam(value = "patientUuid", required = true) String patientUuid, @RequestParam(value = "providerUuid", required = true) String providerUuid) { - if (!Context.getUserContext().hasPrivilege(PrivilegeConstants.DELETE_FORM_DRAFT_PRIVILEGE)) { - log.error("User " + Context.getAuthenticatedUser().getUsername() + - " does not have privilege to discard form drafts"); - return new ResponseEntity<>( - WebUtils.wrapErrorResponse(null, "Insufficient privileges to discard form draft"), - HttpStatus.FORBIDDEN); - } try { formDraftService.discardDraft(patientUuid, providerUuid); return new ResponseEntity<>(HttpStatus.NO_CONTENT); diff --git a/bahmnicore-omod/src/main/resources/liquibase.xml b/bahmnicore-omod/src/main/resources/liquibase.xml index e4f124bc71..fbdad7ddf8 100644 --- a/bahmnicore-omod/src/main/resources/liquibase.xml +++ b/bahmnicore-omod/src/main/resources/liquibase.xml @@ -4792,18 +4792,19 @@ - + - + SELECT COUNT(*) FROM privilege WHERE privilege = 'Delete Form Draft' - Add Delete Form Draft privilege for discarding form drafts - - - - - + Remove Delete Form Draft privilege as discard no longer requires a privilege check + + privilege = 'Delete Form Draft' + + + privilege = 'Delete Form Draft' + From c612b71dfdd0162603c91f0024ca39de4c6e1829 Mon Sep 17 00:00:00 2001 From: Pooja Sastry Date: Fri, 22 May 2026 09:48:01 +0530 Subject: [PATCH 09/13] Pooja | Hive-112427 | Auto-deletion of Draft using scheduler (#33) * Pooja | Hive-112427 | Auto-deletion of Draft using scheduler * Pooja | Hive-112427 | Add test --- .github/workflows/validate_pr.yml | 5 ++- .../module/bahmnicore/dao/FormDraftDAO.java | 6 ++++ .../bahmnicore/dao/impl/FormDraftDaoImpl.java | 16 +++++++++ .../bahmnicore/service/FormDraftService.java | 6 ++++ .../service/impl/FormDraftServiceImpl.java | 5 +++ .../resources/moduleApplicationContext.xml | 36 ------------------- .../impl/FormDraftServiceImplTest.java | 6 ++++ .../task/DiscardAllFormDraftsTask.java | 24 +++++++++++++ .../src/main/resources/liquibase.xml | 13 +++++++ 9 files changed, 78 insertions(+), 39 deletions(-) create mode 100644 bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/task/DiscardAllFormDraftsTask.java diff --git a/.github/workflows/validate_pr.yml b/.github/workflows/validate_pr.yml index d4a8664342..336fb5e46c 100644 --- a/.github/workflows/validate_pr.yml +++ b/.github/workflows/validate_pr.yml @@ -14,10 +14,9 @@ jobs: with: fetch-depth: 0 - name: Set up JDK 1.8 - uses: actions/setup-java@v4 + uses: actions/setup-java@v1 with: - java-version: 8 - distribution: 'zulu' + java-version: 1.8 - name: Cache Maven packages uses: actions/cache@v3 with: diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java index 93d84adb8f..d2b5f73060 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java @@ -33,6 +33,12 @@ public interface FormDraftDAO { */ void deleteLatestDraft(Integer patientId, Integer userId); + /** + * Soft delete (void) all non-voided form drafts. + * Sets voided = true and dateVoided = now, voidedBy = currentUser, voidReason = "Draft deleted by scheduler" + */ + void deleteAllDrafts(); + /** * Retrieve all non-voided, unsaved drafts for a user, ordered newest first. * Drafts where markedAsSaved is true are excluded. diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java index d779d6e3ba..d5ac1ed74c 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java @@ -68,6 +68,22 @@ public void deleteLatestDraft(Integer patientId, Integer userId) throws DAOExcep } } + @Override + public void deleteAllDrafts() throws DAOException { + try { + sessionFactory.getCurrentSession() + .createQuery("UPDATE FormDraft SET voided = true, dateVoided = :now, " + + "voidedBy = :user, voidReason = :reason WHERE voided = false") + .setParameter("now", new Date()) + .setParameter("user", Context.getAuthenticatedUser()) + .setParameter("reason", "Draft deleted by scheduler") + .executeUpdate(); + } catch (Exception e) { + log.error("Error deleting all form drafts", e); + throw new DAOException("Failed to delete all form drafts", e); + } + } + @Override public List getAllByUserOrderedByDateDesc(Integer userId) throws DAOException { try { diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java index 474abc76b4..7ec868b49a 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java @@ -51,6 +51,12 @@ public interface FormDraftService { */ void markDraftAsSaved(String patientUuid, String providerUuid); + /** + * Soft delete (void) all non-voided form drafts regardless of markedAsSaved value. + * Intended to be called by a scheduled task at midnight. + */ + void discardAllDrafts(); + /** * Retrieve a summary list of all unsaved drafts for a given provider. * Reads formData to extract formUuid/formName where available. diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java index 273d41d917..66feba5a96 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java @@ -331,6 +331,11 @@ public void discardDraft(String patientUuid, String providerUuid) { } } + @Override + public void discardAllDrafts() { + formDraftDAO.deleteAllDrafts(); + } + @Override public String getFormData(String formDataPath) { if (formDataPath == null) { diff --git a/bahmnicore-api/src/main/resources/moduleApplicationContext.xml b/bahmnicore-api/src/main/resources/moduleApplicationContext.xml index 81f42bf862..d557cf3919 100644 --- a/bahmnicore-api/src/main/resources/moduleApplicationContext.xml +++ b/bahmnicore-api/src/main/resources/moduleApplicationContext.xml @@ -346,40 +346,4 @@ - - - - - org.bahmni.module.bahmnicore.service.FormDraftService - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java b/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java index 3954498168..b711fe7301 100644 --- a/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java +++ b/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java @@ -264,6 +264,12 @@ public void getDraft_shouldThrowWhenProviderUuidIsEmpty() { formDraftService.getDraft(PATIENT_UUID, ""); } + @Test + public void discardAllDrafts_shouldCallDaoDeleteAllDrafts() { + formDraftService.discardAllDrafts(); + verify(formDraftDAO).deleteAllDrafts(); + } + @Test public void discardDraft_shouldCallDaoDeleteLatestDraft() { Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); diff --git a/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/task/DiscardAllFormDraftsTask.java b/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/task/DiscardAllFormDraftsTask.java new file mode 100644 index 0000000000..5da05da7b6 --- /dev/null +++ b/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/task/DiscardAllFormDraftsTask.java @@ -0,0 +1,24 @@ +package org.bahmni.module.bahmnicore.task; + +import org.bahmni.module.bahmnicore.service.FormDraftService; +import org.openmrs.api.context.Context; +import org.openmrs.scheduler.tasks.AbstractTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DiscardAllFormDraftsTask extends AbstractTask { + + private static final Logger log = LoggerFactory.getLogger(DiscardAllFormDraftsTask.class); + + @Override + public void execute() { + try { + log.info("DiscardAllFormDraftsTask: starting midnight discard of all form drafts"); + FormDraftService formDraftService = Context.getService(FormDraftService.class); + formDraftService.discardAllDrafts(); + log.info("DiscardAllFormDraftsTask: completed successfully"); + } catch (Exception e) { + log.error("DiscardAllFormDraftsTask: failed to discard all form drafts", e); + } + } +} diff --git a/bahmnicore-omod/src/main/resources/liquibase.xml b/bahmnicore-omod/src/main/resources/liquibase.xml index fbdad7ddf8..3f1e6b2b2b 100644 --- a/bahmnicore-omod/src/main/resources/liquibase.xml +++ b/bahmnicore-omod/src/main/resources/liquibase.xml @@ -4792,6 +4792,19 @@ + + + + SELECT COUNT(*) FROM scheduler_task_config WHERE schedulable_class = 'org.bahmni.module.bahmnicore.task.DiscardAllFormDraftsTask' + + + Register scheduled task to discard all form drafts at midnight daily + + INSERT INTO scheduler_task_config(name, schedulable_class, start_time, start_time_pattern, repeat_interval, start_on_startup, started, created_by, date_created, uuid) + VALUES ('Discard All Form Drafts Task', 'org.bahmni.module.bahmnicore.task.DiscardAllFormDraftsTask', DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 DAY), '%Y-%m-%d 00:00:00'), 'MM/dd/yyyy HH:mm:ss', 86400, 1, 1, 1, CURDATE(), UUID()); + + + From 7fa97c7d050736b4b0a5b7761d01263085d6b244 Mon Sep 17 00:00:00 2001 From: Soorya Date: Wed, 19 Aug 2026 13:13:14 +0530 Subject: [PATCH 10/13] Remove the encounter attribute usage as not needed for Form Draft feature --- .../bahmnicore/contract/FormDraftRequest.java | 11 --- .../contract/FormDraftSummaryResponse.java | 11 --- .../module/bahmnicore/model/FormDraft.java | 11 --- .../service/impl/FormDraftServiceImpl.java | 20 ------ .../src/main/resources/FormDraft.hbm.xml | 2 - .../impl/FormDraftServiceImplTest.java | 70 +++---------------- .../src/main/resources/liquibase.xml | 6 -- .../controller/FormDraftControllerTest.java | 7 +- 8 files changed, 13 insertions(+), 125 deletions(-) diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftRequest.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftRequest.java index c16239a5d7..1563f27f6c 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftRequest.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftRequest.java @@ -10,9 +10,6 @@ public class FormDraftRequest { @JsonProperty private String providerUuid; - @JsonProperty - private String encounterUuid; //optional - @JsonProperty private String formData; @@ -35,14 +32,6 @@ public void setProviderUuid(String providerUuid) { this.providerUuid = providerUuid; } - public String getEncounterUuid() { - return encounterUuid; - } - - public void setEncounterUuid(String encounterUuid) { - this.encounterUuid = encounterUuid; - } - public String getFormData() { return formData; } diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftSummaryResponse.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftSummaryResponse.java index e061b2bde9..feb2ad274c 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftSummaryResponse.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/contract/FormDraftSummaryResponse.java @@ -18,9 +18,6 @@ public class FormDraftSummaryResponse { @JsonProperty private String patientIdentifier; - @JsonProperty - private String encounterUuid; - @JsonProperty private String formName; @@ -62,14 +59,6 @@ public void setPatientIdentifier(String patientIdentifier) { this.patientIdentifier = patientIdentifier; } - public String getEncounterUuid() { - return encounterUuid; - } - - public void setEncounterUuid(String encounterUuid) { - this.encounterUuid = encounterUuid; - } - public String getFormName() { return formName; } diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/model/FormDraft.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/model/FormDraft.java index 373930f980..fe74e4f5e7 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/model/FormDraft.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/model/FormDraft.java @@ -1,7 +1,6 @@ package org.bahmni.module.bahmnicore.model; import org.openmrs.BaseChangeableOpenmrsData; -import org.openmrs.Encounter; import org.openmrs.Patient; import org.openmrs.User; @@ -13,8 +12,6 @@ public class FormDraft extends BaseChangeableOpenmrsData { private Patient patient; - private Encounter encounter; // nullable — populated once encounter is created - private User user; private String formDataPath; // Path to JSON file on filesystem @@ -50,14 +47,6 @@ public void setPatient(Patient patient) { this.patient = patient; } - public Encounter getEncounter() { - return encounter; - } - - public void setEncounter(Encounter encounter) { - this.encounter = encounter; - } - public User getUser() { return user; } diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java index 66feba5a96..ed61b3e0b8 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java @@ -19,12 +19,10 @@ import org.bahmni.module.bahmnicore.dao.FormDraftDAO; import org.bahmni.module.bahmnicore.model.FormDraft; import org.bahmni.module.bahmnicore.service.FormDraftService; -import org.openmrs.Encounter; import org.openmrs.Patient; import org.openmrs.Provider; import org.openmrs.User; import org.openmrs.api.APIException; -import org.openmrs.api.EncounterService; import org.openmrs.api.PatientService; import org.openmrs.api.ProviderService; import org.openmrs.api.UserService; @@ -45,7 +43,6 @@ public class FormDraftServiceImpl implements FormDraftService { private PatientService patientService; private UserService userService; private ProviderService providerService; - private EncounterService encounterService; private User authenticatedUser; private String formDraftsBasePath; @@ -78,11 +75,6 @@ public void setProviderService(ProviderService providerService) { this.providerService = providerService; } - @Autowired(required = false) - public void setEncounterService(EncounterService encounterService) { - this.encounterService = encounterService; - } - protected void setFormDraftsBasePath(String basePath) { this.formDraftsBasePath = basePath; } @@ -153,16 +145,6 @@ public FormDraft saveDraft(FormDraftRequest request) { draft.setPatient(patient); draft.setUser(user); - if (request.getEncounterUuid() != null && !request.getEncounterUuid().isEmpty()) { - EncounterService es = encounterService != null ? encounterService : Context.getEncounterService(); - Encounter encounter = es.getEncounterByUuid(request.getEncounterUuid()); - if (encounter != null) { - draft.setEncounter(encounter); - } else { - log.warn("Encounter UUID provided but not found: " + request.getEncounterUuid()); - } - } - String filePath = generateFilePath(draft.getUuid()); if (isNewDraft || contentChanged) { writeFormDataToFile(filePath, request.getFormData()); @@ -390,7 +372,6 @@ private FormDraftSummaryResponse buildSummary(FormDraft draft) { String patientIdentifier = patient.getPatientIdentifier() != null ? patient.getPatientIdentifier().getIdentifier() : null; - String encounterUuid = draft.getEncounter() != null ? draft.getEncounter().getUuid() : null; long timestamp = draft.getDateChanged() != null ? draft.getDateChanged().getTime() : draft.getDateCreated().getTime(); @@ -402,7 +383,6 @@ private FormDraftSummaryResponse buildSummary(FormDraft draft) { response.setPatientUuid(patient.getUuid()); response.setPatientName(patientName); response.setPatientIdentifier(patientIdentifier); - response.setEncounterUuid(encounterUuid); response.setFormName(formName); response.setTimestamp(timestamp); return response; diff --git a/bahmnicore-api/src/main/resources/FormDraft.hbm.xml b/bahmnicore-api/src/main/resources/FormDraft.hbm.xml index 93ce08a22b..61442061d9 100644 --- a/bahmnicore-api/src/main/resources/FormDraft.hbm.xml +++ b/bahmnicore-api/src/main/resources/FormDraft.hbm.xml @@ -15,8 +15,6 @@ not-null="true" length="38" unique="true"/> - inv.getArguments()[0]); - - FormDraft result = formDraftService.saveDraft(request); - - assertEquals(encounter, result.getEncounter()); - } - - @Test - public void saveDraft_shouldNotFailWhenEncounterUuidNotFound() { - FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, "nonexistent-encounter", "{\"form\":\"data\"}"); - Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); - User user = buildUser(PROVIDER_UUID, PROVIDER_ID); - - when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient); - mockProviderResolution(user); - when(formDraftDAO.getLatestByPatientAndUser(PATIENT_ID, PROVIDER_ID)).thenReturn(null); - when(encounterService.getEncounterByUuid("nonexistent-encounter")).thenReturn(null); - when(formDraftDAO.saveOrUpdate(any(FormDraft.class))).thenAnswer(inv -> inv.getArguments()[0]); - - FormDraft result = formDraftService.saveDraft(request); - - assertNull(result.getEncounter()); - } - @Test(expected = IllegalArgumentException.class) public void saveDraft_shouldThrowWhenPatientUuidIsNull() { - FormDraftRequest request = buildRequest(null, PROVIDER_UUID, null, "{\"form\":\"data\"}"); + FormDraftRequest request = buildRequest(null, PROVIDER_UUID, "{\"form\":\"data\"}"); formDraftService.saveDraft(request); } @Test(expected = IllegalArgumentException.class) public void saveDraft_shouldThrowWhenProviderUuidIsEmpty() { - FormDraftRequest request = buildRequest(PATIENT_UUID, "", null, "{\"form\":\"data\"}"); + FormDraftRequest request = buildRequest(PATIENT_UUID, "", "{\"form\":\"data\"}"); formDraftService.saveDraft(request); } @Test(expected = IllegalArgumentException.class) public void saveDraft_shouldThrowWhenFormDataIsNull() { - FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null, null); + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null); formDraftService.saveDraft(request); } @Test public void saveDraft_shouldPersistFormDataPath() { - FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null, "{\"form\":\"data\"}"); + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, "{\"form\":\"data\"}"); Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); User user = buildUser(PROVIDER_UUID, PROVIDER_ID); @@ -370,7 +327,7 @@ public void markDraftAsSaved_shouldThrowWhenProviderNotFound() { @Test public void saveDraft_shouldResolveUserViaProvider() { - FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null, "{\"form\":\"data\"}"); + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, "{\"form\":\"data\"}"); Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); User user = buildUser("user-uuid-999", PROVIDER_ID); @@ -387,7 +344,7 @@ public void saveDraft_shouldResolveUserViaProvider() { @Test public void saveDraft_shouldCreateNewDraftWhenExistingDraftIsMarkedAsSaved() { - FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null, "{\"updated\":\"data\"}"); + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, "{\"updated\":\"data\"}"); Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); User user = buildUser(PROVIDER_UUID, PROVIDER_ID); @@ -413,7 +370,7 @@ public void saveDraft_shouldCreateNewDraftWhenExistingDraftIsMarkedAsSaved() { @Test public void saveDraft_shouldInitializeMarkedAsSavedAsFalseForNewDraft() { - FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, null, "{\"form\":\"data\"}"); + FormDraftRequest request = buildRequest(PATIENT_UUID, PROVIDER_UUID, "{\"form\":\"data\"}"); Patient patient = buildPatient(PATIENT_UUID, PATIENT_ID); User user = buildUser(PROVIDER_UUID, PROVIDER_ID); @@ -437,8 +394,6 @@ public void getDraftsByProvider_returnsDraftsNewestFirst() throws Exception { mockProviderResolution(user); Patient patient = buildPatientWithDetails(PATIENT_UUID, PATIENT_ID, "John Doe", "ET001"); - Encounter encounter = new Encounter(); - encounter.setUuid(ENCOUNTER_UUID); // formData is a serialized observations array; formName is derived from formFieldPath prefix File formDataFile = temporaryFolder.newFile("draft-form-identity.json"); @@ -447,14 +402,12 @@ public void getDraftsByProvider_returnsDraftsNewestFirst() throws Exception { FormDraft draftOlder = new FormDraft(); draftOlder.setUuid("draft-uuid-older"); draftOlder.setPatient(patient); - draftOlder.setEncounter(encounter); draftOlder.setFormDataPath(formDataFile.getAbsolutePath()); draftOlder.setDateCreated(new java.util.Date(1000L)); FormDraft draftNewer = new FormDraft(); draftNewer.setUuid("draft-uuid-newer"); draftNewer.setPatient(patient); - draftNewer.setEncounter(null); draftNewer.setFormDataPath(formDataFile.getAbsolutePath()); draftNewer.setDateCreated(new java.util.Date(2000L)); draftNewer.setDateChanged(new java.util.Date(3000L)); @@ -466,11 +419,9 @@ public void getDraftsByProvider_returnsDraftsNewestFirst() throws Exception { assertEquals(2, results.size()); assertEquals("draft-uuid-newer", results.get(0).getDraftUuid()); assertEquals(3000L, (long) results.get(0).getTimestamp()); - assertNull(results.get(0).getEncounterUuid()); assertEquals("Vitals", results.get(0).getFormName()); assertEquals("draft-uuid-older", results.get(1).getDraftUuid()); assertEquals(1000L, (long) results.get(1).getTimestamp()); - assertEquals(ENCOUNTER_UUID, results.get(1).getEncounterUuid()); } @Test @@ -576,11 +527,10 @@ private void mockProviderResolution(User user) { when(userService.getUsersByPerson(person, false)).thenReturn(Collections.singletonList(user)); } - private FormDraftRequest buildRequest(String patientUuid, String providerUuid, String encounterUuid, String formData) { + private FormDraftRequest buildRequest(String patientUuid, String providerUuid, String formData) { FormDraftRequest request = new FormDraftRequest(); request.setPatientUuid(patientUuid); request.setProviderUuid(providerUuid); - request.setEncounterUuid(encounterUuid); request.setFormData(formData); return request; } diff --git a/bahmnicore-omod/src/main/resources/liquibase.xml b/bahmnicore-omod/src/main/resources/liquibase.xml index 3f1e6b2b2b..204511e0f4 100644 --- a/bahmnicore-omod/src/main/resources/liquibase.xml +++ b/bahmnicore-omod/src/main/resources/liquibase.xml @@ -4730,9 +4730,6 @@ - - - @@ -4770,9 +4767,6 @@ - diff --git a/bahmnicore-omod/src/test/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftControllerTest.java b/bahmnicore-omod/src/test/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftControllerTest.java index 9a47f40015..654d35c3f2 100644 --- a/bahmnicore-omod/src/test/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftControllerTest.java +++ b/bahmnicore-omod/src/test/java/org/bahmni/module/bahmnicore/web/v1_0/controller/FormDraftControllerTest.java @@ -65,7 +65,7 @@ public void getDraft_shouldReturnBadRequestWhenServiceThrowsException() { @Test public void saveDraft_shouldReturnBadRequestWhenValidationFails() { - FormDraftRequest request = buildFormDraftRequest(null, PROVIDER_UUID, null, "{\"form\":\"data\"}"); + FormDraftRequest request = buildFormDraftRequest(null, PROVIDER_UUID, "{\"form\":\"data\"}"); doThrow(new IllegalArgumentException("Patient UUID is required")).when(formDraftService).saveDraft(any(FormDraftRequest.class)); ResponseEntity response = controller.saveDraft(request); @@ -75,7 +75,7 @@ public void saveDraft_shouldReturnBadRequestWhenValidationFails() { @Test public void saveDraft_shouldReturnBadRequestWhenServiceThrowsException() { - FormDraftRequest request = buildFormDraftRequest(PATIENT_UUID, PROVIDER_UUID, null, "{\"form\":\"data\"}"); + FormDraftRequest request = buildFormDraftRequest(PATIENT_UUID, PROVIDER_UUID, "{\"form\":\"data\"}"); doThrow(new RuntimeException("Unexpected error")).when(formDraftService).saveDraft(any(FormDraftRequest.class)); ResponseEntity response = controller.saveDraft(request); @@ -156,11 +156,10 @@ public void getDraftsByProvider_returns400_whenProviderUuidIsInvalid() { // --- Helpers --- - private FormDraftRequest buildFormDraftRequest(String patientUuid, String providerUuid, String encounterUuid, String formData) { + private FormDraftRequest buildFormDraftRequest(String patientUuid, String providerUuid, String formData) { FormDraftRequest request = new FormDraftRequest(); request.setPatientUuid(patientUuid); request.setProviderUuid(providerUuid); - request.setEncounterUuid(encounterUuid); request.setFormData(formData); return request; } From 0943abf9d8b603da2c486a1912ecf17f91677a28 Mon Sep 17 00:00:00 2001 From: Pooja Sastry Date: Wed, 12 Aug 2026 15:30:11 +0530 Subject: [PATCH 11/13] Pooja | Hive-123853 | [Tech Debt] Deletion of Drafts after a Retention period (#46) * Pooja | Hive-123853 | Deletion of Voided Drafts * Pooja | Hive-123853 | Remove unnecessary comments and failing test * Pooja | Hive-123853 | Update liquibase changeset * Pooja | Hive-123853 | Addressed PR review comments * Pooja | Hive-123853 | Update code to throw error in case default retention days is not set --- .../module/bahmnicore/dao/FormDraftDAO.java | 12 +++++++++-- .../bahmnicore/dao/impl/FormDraftDaoImpl.java | 20 +++++++++++++++++++ .../bahmnicore/service/FormDraftService.java | 8 ++++++++ .../service/impl/FormDraftServiceImpl.java | 18 +++++++++++++++++ .../impl/FormDraftServiceImplTest.java | 1 + .../task/DiscardAllFormDraftsTask.java | 12 +++++++++-- .../src/main/resources/liquibase.xml | 13 ++++++++++++ 7 files changed, 80 insertions(+), 4 deletions(-) diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java index d2b5f73060..a969099448 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/FormDraftDAO.java @@ -1,9 +1,9 @@ package org.bahmni.module.bahmnicore.dao; -import org.bahmni.module.bahmnicore.model.FormDraft; - import java.util.List; +import org.bahmni.module.bahmnicore.model.FormDraft; + public interface FormDraftDAO { /** @@ -47,4 +47,12 @@ public interface FormDraftDAO { * @return list of FormDraft objects, ordered by COALESCE(dateChanged, dateCreated) DESC */ List getAllByUserOrderedByDateDesc(Integer userId); + + /** + * Permanently delete (hard delete) all form drafts older than the specified number of days. + * + * @param retentionDays the number of days to retain drafts + * @return the number of draft records deleted + */ + Integer deleteDraftsOlderThanDays(Integer retentionDays); } diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java index d5ac1ed74c..0a39d042cf 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/dao/impl/FormDraftDaoImpl.java @@ -9,6 +9,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Calendar; import java.util.Date; import java.util.List; @@ -99,4 +100,23 @@ public List getAllByUserOrderedByDateDesc(Integer userId) throws DAOE throw new DAOException("Failed to retrieve form drafts for user", e); } } + + @Override + public Integer deleteDraftsOlderThanDays(Integer retentionDays) throws DAOException { + try { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_MONTH, -retentionDays); + Date cutoffDate = calendar.getTime(); + + Integer deletedCount = sessionFactory.getCurrentSession() + .createQuery("DELETE FROM FormDraft WHERE dateCreated < :cutoffDate") + .setParameter("cutoffDate", cutoffDate) + .executeUpdate(); + log.info("Deleted {} form drafts older than {} days", deletedCount, retentionDays); + return deletedCount; + } catch (Exception e) { + log.error("Error deleting form drafts older than {} days", retentionDays, e); + throw new DAOException("Failed to delete form drafts", e); + } + } } diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java index 7ec868b49a..8ef333691e 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/FormDraftService.java @@ -66,4 +66,12 @@ public interface FormDraftService { * @return list of FormDraftSummaryResponse, ordered newest first; empty list if provider not found */ List getDraftsByProvider(String providerUuid); + + /** + * Delete all form drafts older than the configured retention period, regardless of voided status. + * The retention period is read from global property 'bahmni.formDraft.voidedRetentionDays'. + * The property is initialized to 15 days by the Liquibase changeset during module deployment. + * Intended to be called by a scheduled task at midnight. + */ + void deleteDraftsOlderThanRetentionPeriod(); } diff --git a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java index ed61b3e0b8..0eb5141a8e 100644 --- a/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java +++ b/bahmnicore-api/src/main/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImpl.java @@ -38,6 +38,7 @@ public class FormDraftServiceImpl implements FormDraftService { private static final Logger log = LoggerFactory.getLogger(FormDraftServiceImpl.class); private static final String FORM_DRAFTS_SUBDIRECTORY = "form_draft"; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final String VOIDED_RETENTION_DAYS_PROPERTY = "bahmni.formDraft.voidedRetentionDays"; private FormDraftDAO formDraftDAO; private PatientService patientService; @@ -450,4 +451,21 @@ public void markDraftAsSaved(String patientUuid, String providerUuid) { throw new RuntimeException("Failed to mark form draft as saved: " + e.getMessage(), e); } } + + @Override + public void deleteDraftsOlderThanRetentionPeriod() { + try { + String retentionDaysStr = Context.getAdministrationService() + .getGlobalProperty(VOIDED_RETENTION_DAYS_PROPERTY); + if (retentionDaysStr == null) { + throw new IllegalStateException("Global property '" + VOIDED_RETENTION_DAYS_PROPERTY + "' is not set"); + } + Integer retentionDays = Integer.parseInt(retentionDaysStr); + Integer deletedCount = formDraftDAO.deleteDraftsOlderThanDays(retentionDays); + log.info("Deleted {} form drafts older than {} days", deletedCount, retentionDays); + } catch (Exception e) { + log.error("Error deleting form drafts by retention period", e); + throw new RuntimeException("Failed to delete form drafts: " + e.getMessage(), e); + } + } } diff --git a/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java b/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java index b6e947fb62..0dde16cb89 100644 --- a/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java +++ b/bahmnicore-api/src/test/java/org/bahmni/module/bahmnicore/service/impl/FormDraftServiceImplTest.java @@ -30,6 +30,7 @@ import org.openmrs.api.PatientService; import org.openmrs.api.ProviderService; import org.openmrs.api.UserService; +import org.openmrs.api.context.Context; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/task/DiscardAllFormDraftsTask.java b/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/task/DiscardAllFormDraftsTask.java index 5da05da7b6..d8b5bb27b0 100644 --- a/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/task/DiscardAllFormDraftsTask.java +++ b/bahmnicore-omod/src/main/java/org/bahmni/module/bahmnicore/task/DiscardAllFormDraftsTask.java @@ -13,12 +13,20 @@ public class DiscardAllFormDraftsTask extends AbstractTask { @Override public void execute() { try { - log.info("DiscardAllFormDraftsTask: starting midnight discard of all form drafts"); + log.info("DiscardAllFormDraftsTask: starting midnight task"); + FormDraftService formDraftService = Context.getService(FormDraftService.class); + + + log.debug("DiscardAllFormDraftsTask: discarding all non-voided drafts"); formDraftService.discardAllDrafts(); + + log.debug("DiscardAllFormDraftsTask: deleting drafts older than retention period"); + formDraftService.deleteDraftsOlderThanRetentionPeriod(); + log.info("DiscardAllFormDraftsTask: completed successfully"); } catch (Exception e) { - log.error("DiscardAllFormDraftsTask: failed to discard all form drafts", e); + log.error("DiscardAllFormDraftsTask: failed during execution", e); } } } diff --git a/bahmnicore-omod/src/main/resources/liquibase.xml b/bahmnicore-omod/src/main/resources/liquibase.xml index 204511e0f4..4c9841c57e 100644 --- a/bahmnicore-omod/src/main/resources/liquibase.xml +++ b/bahmnicore-omod/src/main/resources/liquibase.xml @@ -4814,4 +4814,17 @@ + + + + SELECT COUNT(*) FROM global_property WHERE property = 'bahmni.formDraft.voidedRetentionDays' + + + Add global property for form draft retention period (days to retain before permanent deletion) + + insert into global_property (`property`, `property_value`, `description`, `uuid`) + values ('bahmni.formDraft.voidedRetentionDays', '15', 'Number of days to retain form drafts before permanent deletion. Default is 15 days.', uuid()); + + + From 5103bcc97ad3ed1faeba3be09a04a479212b819f Mon Sep 17 00:00:00 2001 From: Pooja Sastry Date: Wed, 26 Aug 2026 19:09:20 +0530 Subject: [PATCH 12/13] Hive-127888 | [Product PR feedback] Add toggle to have the option to discard drafts on Save instead of setting markAsSaved as true. --- bahmnicore-omod/src/main/resources/liquibase.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bahmnicore-omod/src/main/resources/liquibase.xml b/bahmnicore-omod/src/main/resources/liquibase.xml index 4c9841c57e..b0c6433141 100644 --- a/bahmnicore-omod/src/main/resources/liquibase.xml +++ b/bahmnicore-omod/src/main/resources/liquibase.xml @@ -4827,4 +4827,17 @@ + + + + SELECT COUNT(*) FROM global_property WHERE property = 'bahmni.formDraft.discardOnSave' + + + Add global property to control whether saving a consultation discards the draft instead of marking it as saved + + insert into global_property (`property`, `property_value`, `description`, `uuid`) + values ('bahmni.formDraft.discardOnSave', 'false', 'When true, saving a consultation discards the auto-saved draft (DELETE) instead of marking it as saved (PATCH). Default is false.', uuid()); + + + From 5bc8f81fee95769241a8c835c2326bb220d2755b Mon Sep 17 00:00:00 2001 From: Pooja Sastry Date: Wed, 2 Sep 2026 15:10:19 +0530 Subject: [PATCH 13/13] BAH-4874 | Setting discardOnSave to true for Bahmni --- bahmnicore-omod/src/main/resources/liquibase.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bahmnicore-omod/src/main/resources/liquibase.xml b/bahmnicore-omod/src/main/resources/liquibase.xml index b0c6433141..f076f78bad 100644 --- a/bahmnicore-omod/src/main/resources/liquibase.xml +++ b/bahmnicore-omod/src/main/resources/liquibase.xml @@ -4827,7 +4827,7 @@ - + SELECT COUNT(*) FROM global_property WHERE property = 'bahmni.formDraft.discardOnSave' @@ -4836,7 +4836,7 @@ Add global property to control whether saving a consultation discards the draft instead of marking it as saved insert into global_property (`property`, `property_value`, `description`, `uuid`) - values ('bahmni.formDraft.discardOnSave', 'false', 'When true, saving a consultation discards the auto-saved draft (DELETE) instead of marking it as saved (PATCH). Default is false.', uuid()); + values ('bahmni.formDraft.discardOnSave', 'true', 'When true, saving a consultation discards the auto-saved draft (DELETE) instead of marking it as saved (PATCH). Default is false.', uuid());