Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,24 @@ public void handle(Encounter encounter, User user, Date date, String s) {
List<BedPatientAssignment> bpaList = bedManagementService.getBedPatientAssignmentByEncounter(encounter.getUuid(),
true);

// The bpa instances below are already persistent and attached to the current
// Hibernate
// session, so mutating them is enough: the changes are flushed with the
// surrounding
// transaction. We deliberately avoid calling the @Transactional
// bedManagementService
// save/delete methods here, because their commit forces an early flush of the
// whole
// session and can trip over unrelated, not-yet-populated entities (e.g. a
// VisitAttribute
// whose NOT NULL valueReference has not been serialized yet).
if (encounter.getVoided()) {
log.debug("Voiding beds due to voided encounter");
for (BedPatientAssignment bpa : bpaList) {
bedManagementService.deleteBedPatientAssignment(bpa, "encounter voided");
bpa.setVoided(true);
bpa.setDateVoided(date);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what date is passed in here, I would think this should always be the current datetime? (maybe it already is)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct @mogoodrich :

Yes, for a SaveHandler the date is always the current datetime

SaveHandlers are driven by RequiredDataAdvice (the MethodBeforeAdvice on save* service methods). The save path goes through the 3-arg recursivelyHandle(Class, OpenmrsObject, String) overload, whose bytecode is unambiguous:

2: invokestatic Context.getAuthenticatedUser() // -> user
5: new java/util/Date
9: invokespecial java/util/Date."":()V // -> new Date() == "now"
17: invokestatic recursivelyHandle(Class, OpenmrsObject, User, Date, String, Set)

That new Date() is then handed straight to each handler via RequiredDataHandler.handle(object, user, date, reason) (the #150 interface call at offset 71). So the date your handler receives is a fresh new Date() captured at the start of the save operation — i.e. the current datetime.

That makes bpa.setDateVoided(date) equivalent to the original deleteBedPatientAssignment's bpa.setDateVoided(new Date()) — just sourced from the timestamp the framework already created for this save, so the void instant lines up with the rest of that operation's audit metadata. No behavior change, and no risk of it being null.

One nuance worth knowing (doesn't affect us)

The other branches in RequiredDataAdvice — the Voidable/Retireable paths that drive VoidHandler/RetireHandler — behave differently: they pass the object's existing getDateVoided() / getDateRetired() if it's already set, and only fall back to new Date() otherwise (visible at offsets 253–303 and 384–434). So for those handler types, the date is "whenever it was voided," not necessarily now.

But our class is a SaveHandler, which is only ever invoked through the save path → always new Date(). So your instinct is correct: for this handler it's always the current datetime.

No code change needed — the date parameter is the right thing to use here.

bpa.setVoidReason("encounter voided");
bpa.setVoidedBy(user);
log.debug("Voided bedPatientAssignment for bed " + bpa.getBed());
}
}
Expand All @@ -60,7 +74,6 @@ public void handle(Encounter encounter, User user, Date date, String s) {
for (BedPatientAssignment bpa : bpaList) {
log.debug("updating bedPatientAssignment starttime for bed " + bpa.getBed());
bpa.setStartDatetime(encounterDatetime);
bedManagementService.saveBedPatientAssignment(bpa);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package org.openmrs.module.bedmanagement.aop;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import java.util.Collections;
import java.util.Date;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openmrs.Encounter;
import org.openmrs.User;
import org.openmrs.module.bedmanagement.entity.BedPatientAssignment;
import org.openmrs.module.bedmanagement.service.BedManagementService;

/**
* Unit tests for {@link EncounterWithBedPatientAssignmentSaveHandler}.
* <p>
* The handler runs inside the OpenMRS save pipeline, so the bed assignments it touches are already
* persistent and attached to the current Hibernate session. It must mutate them in place and let
* the surrounding transaction flush the changes. It must NOT call the {@code @Transactional}
* {@code saveBedPatientAssignment} / {@code deleteBedPatientAssignment} service methods: their
* commit forced an early flush of the whole session, which could trip over unrelated,
* not-yet-populated entities (e.g. a {@code VisitAttribute} whose NOT NULL {@code valueReference}
* had not been serialized yet) and abort the save with a {@code DataIntegrityViolationException}.
*/
@ExtendWith(MockitoExtension.class)
public class EncounterWithBedPatientAssignmentSaveHandlerUnitTest {

@Mock
private BedManagementService bedManagementService;

private EncounterWithBedPatientAssignmentSaveHandler handler;

private final User user = new User();

@BeforeEach
public void setUp() {
handler = new EncounterWithBedPatientAssignmentSaveHandler(bedManagementService);
}

@Test
public void handleShouldUpdateAssignmentStartDatetimeInPlaceWithoutCallingTransactionalSave() {
Date encounterDatetime = new Date();

Check warning on line 53 in api/src/test/java/org/openmrs/module/bedmanagement/aop/EncounterWithBedPatientAssignmentSaveHandlerUnitTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use the system clock in tests.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-module-bedmanagement&issues=AZ7LiT-4SK4XefZ-rixM&open=AZ7LiT-4SK4XefZ-rixM&pullRequest=116
Encounter encounter = new Encounter();
encounter.setEncounterId(1001);
encounter.setUuid("enc-uuid");
encounter.setEncounterDatetime(encounterDatetime);

BedPatientAssignment bpa = new BedPatientAssignment();
when(bedManagementService.getBedPatientAssignmentByEncounter("enc-uuid", true))
.thenReturn(Collections.singletonList(bpa));

handler.handle(encounter, user, new Date(), null);

Check warning on line 63 in api/src/test/java/org/openmrs/module/bedmanagement/aop/EncounterWithBedPatientAssignmentSaveHandlerUnitTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use the system clock in tests.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-module-bedmanagement&issues=AZ7LiT-4SK4XefZ-rixN&open=AZ7LiT-4SK4XefZ-rixN&pullRequest=116

assertEquals(encounterDatetime, bpa.getStartDatetime());
// the fix: the assignment is mutated in place, never re-saved through the
// transactional service
verify(bedManagementService, never()).saveBedPatientAssignment(any());
}

@Test
public void handleShouldVoidAssignmentsInPlaceWhenEncounterVoidedWithoutCallingTransactionalDelete() {
Encounter encounter = new Encounter();
encounter.setEncounterId(1001);
encounter.setUuid("enc-uuid");
encounter.setEncounterDatetime(new Date());

Check warning on line 76 in api/src/test/java/org/openmrs/module/bedmanagement/aop/EncounterWithBedPatientAssignmentSaveHandlerUnitTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use the system clock in tests.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-module-bedmanagement&issues=AZ7LiT-4SK4XefZ-rixO&open=AZ7LiT-4SK4XefZ-rixO&pullRequest=116
encounter.setVoided(true);

BedPatientAssignment bpa = new BedPatientAssignment();
when(bedManagementService.getBedPatientAssignmentByEncounter("enc-uuid", true))
.thenReturn(Collections.singletonList(bpa));

Date voidDate = new Date();

Check warning on line 83 in api/src/test/java/org/openmrs/module/bedmanagement/aop/EncounterWithBedPatientAssignmentSaveHandlerUnitTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use the system clock in tests.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-module-bedmanagement&issues=AZ7LiT-4SK4XefZ-rixP&open=AZ7LiT-4SK4XefZ-rixP&pullRequest=116
handler.handle(encounter, user, voidDate, null);

assertTrue(bpa.getVoided());
assertEquals("encounter voided", bpa.getVoidReason());
assertEquals(user, bpa.getVoidedBy());
assertEquals(voidDate, bpa.getDateVoided());
// the fix: the assignment is voided in place, never routed through the
// transactional service
verify(bedManagementService, never()).deleteBedPatientAssignment(any(), anyString());
verify(bedManagementService, never()).saveBedPatientAssignment(any());
}

@Test
public void handleShouldDoNothingForTransientEncounter() {
Encounter encounter = new Encounter(); // no encounterId -> not yet persisted

handler.handle(encounter, user, new Date(), null);

Check warning on line 100 in api/src/test/java/org/openmrs/module/bedmanagement/aop/EncounterWithBedPatientAssignmentSaveHandlerUnitTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use the system clock in tests.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-module-bedmanagement&issues=AZ7LiT-4SK4XefZ-rixQ&open=AZ7LiT-4SK4XefZ-rixQ&pullRequest=116

verifyNoInteractions(bedManagementService);
}
}