From 675efcf0e16090e7aec8b1335c6cea53cda06669 Mon Sep 17 00:00:00 2001 From: Szymon Radziszewski Date: Tue, 16 Apr 2024 00:18:18 +0200 Subject: [PATCH 1/2] OAM-46: Added Ward entity with CRUD --- .../WardRepositoryIntegrationTest.java | 104 ++++++ .../web/BaseWebIntegrationTest.java | 4 + .../web/WardControllerIntegrationTest.java | 309 ++++++++++++++++++ .../referencedata/domain/RightName.java | 1 + .../openlmis/referencedata/domain/Ward.java | 163 +++++++++ .../openlmis/referencedata/dto/WardDto.java | 62 ++++ .../errorhandling/RefDataErrorHandling.java | 2 + .../repository/WardRepository.java | 49 +++ .../custom/WardRepositoryCustom.java | 27 ++ .../custom/impl/WardRepositoryCustomImpl.java | 133 ++++++++ .../util/messagekeys/MessageKeys.java | 1 + .../util/messagekeys/WardMessageKeys.java | 26 ++ .../referencedata/web/WardController.java | 212 ++++++++++++ src/main/resources/api-definition.yaml | 138 ++++++++ .../20240410074547750__create_wards_table.sql | 10 + ...40415084147861__add_wards_manage_right.sql | 1 + src/main/resources/messages_en.properties | 4 + src/main/resources/schemas/ward.json | 35 ++ src/main/resources/schemas/wardPage.json | 60 ++++ .../referencedata/domain/WardTest.java | 62 ++++ .../referencedata/dto/WardDtoTest.java | 29 ++ .../testbuilder/WardDataBuilder.java | 117 +++++++ 22 files changed, 1549 insertions(+) create mode 100644 src/integration-test/java/org/openlmis/referencedata/repository/WardRepositoryIntegrationTest.java create mode 100644 src/integration-test/java/org/openlmis/referencedata/web/WardControllerIntegrationTest.java create mode 100644 src/main/java/org/openlmis/referencedata/domain/Ward.java create mode 100644 src/main/java/org/openlmis/referencedata/dto/WardDto.java create mode 100644 src/main/java/org/openlmis/referencedata/repository/WardRepository.java create mode 100644 src/main/java/org/openlmis/referencedata/repository/custom/WardRepositoryCustom.java create mode 100644 src/main/java/org/openlmis/referencedata/repository/custom/impl/WardRepositoryCustomImpl.java create mode 100644 src/main/java/org/openlmis/referencedata/util/messagekeys/WardMessageKeys.java create mode 100644 src/main/java/org/openlmis/referencedata/web/WardController.java create mode 100644 src/main/resources/db/migration/20240410074547750__create_wards_table.sql create mode 100644 src/main/resources/db/migration/20240415084147861__add_wards_manage_right.sql create mode 100644 src/main/resources/schemas/ward.json create mode 100644 src/main/resources/schemas/wardPage.json create mode 100644 src/test/java/org/openlmis/referencedata/domain/WardTest.java create mode 100644 src/test/java/org/openlmis/referencedata/dto/WardDtoTest.java create mode 100644 src/test/java/org/openlmis/referencedata/testbuilder/WardDataBuilder.java diff --git a/src/integration-test/java/org/openlmis/referencedata/repository/WardRepositoryIntegrationTest.java b/src/integration-test/java/org/openlmis/referencedata/repository/WardRepositoryIntegrationTest.java new file mode 100644 index 00000000..e51c9c1d --- /dev/null +++ b/src/integration-test/java/org/openlmis/referencedata/repository/WardRepositoryIntegrationTest.java @@ -0,0 +1,104 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org. + */ + +package org.openlmis.referencedata.repository; + +import java.util.UUID; +import org.junit.Test; +import org.openlmis.referencedata.domain.Facility; +import org.openlmis.referencedata.domain.FacilityType; +import org.openlmis.referencedata.domain.GeographicLevel; +import org.openlmis.referencedata.domain.GeographicZone; +import org.openlmis.referencedata.domain.Ward; +import org.openlmis.referencedata.testbuilder.FacilityDataBuilder; +import org.openlmis.referencedata.testbuilder.FacilityTypeDataBuilder; +import org.openlmis.referencedata.testbuilder.GeographicLevelDataBuilder; +import org.openlmis.referencedata.testbuilder.GeographicZoneDataBuilder; +import org.openlmis.referencedata.testbuilder.WardDataBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.repository.CrudRepository; + +@SuppressWarnings("PMD.TooManyMethods") +public class WardRepositoryIntegrationTest extends BaseCrudRepositoryIntegrationTest { + + @Autowired + private WardRepository repository; + + @Autowired + private GeographicLevelRepository geographicLevelRepository; + + @Autowired + private GeographicZoneRepository geographicZoneRepository; + + @Autowired + private FacilityTypeRepository facilityTypeRepository; + + @Autowired + private FacilityRepository facilityRepository; + + @Override + CrudRepository getRepository() { + return repository; + } + + @Override + Ward generateInstance() { + return new WardDataBuilder() + .withFacility(generateFacility()) + .buildAsNew(); + } + + @Test(expected = DataIntegrityViolationException.class) + public void shouldNotAllowForSeveralWardsWithSameCode() { + Ward ward1 = generateInstance(); + Ward ward2 = generateInstance(); + ward1.setCode(ward2.getCode()); + + repository.saveAndFlush(ward1); + repository.saveAndFlush(ward2); + } + + private Facility generateFacility() { + Facility facility = new FacilityDataBuilder() + .withGeographicZone(generateGeographicZone()) + .withType(generateFacilityType()) + .withoutOperator() + .buildAsNew(); + + facilityRepository.save(facility); + return facility; + } + + private GeographicLevel generateGeographicLevel() { + GeographicLevel geographicLevel = new GeographicLevelDataBuilder().buildAsNew(); + geographicLevelRepository.save(geographicLevel); + return geographicLevel; + } + + private GeographicZone generateGeographicZone() { + GeographicZone geographicZone = + new GeographicZoneDataBuilder().withLevel(generateGeographicLevel()).buildAsNew(); + geographicZoneRepository.save(geographicZone); + return geographicZone; + } + + private FacilityType generateFacilityType() { + FacilityType facilityType = new FacilityTypeDataBuilder().buildAsNew(); + facilityTypeRepository.save(facilityType); + return facilityType; + } + +} diff --git a/src/integration-test/java/org/openlmis/referencedata/web/BaseWebIntegrationTest.java b/src/integration-test/java/org/openlmis/referencedata/web/BaseWebIntegrationTest.java index d8724c14..d57fe84a 100644 --- a/src/integration-test/java/org/openlmis/referencedata/web/BaseWebIntegrationTest.java +++ b/src/integration-test/java/org/openlmis/referencedata/web/BaseWebIntegrationTest.java @@ -89,6 +89,7 @@ import org.openlmis.referencedata.repository.SystemNotificationRepository; import org.openlmis.referencedata.repository.TradeItemRepository; import org.openlmis.referencedata.repository.UserRepository; +import org.openlmis.referencedata.repository.WardRepository; import org.openlmis.referencedata.repository.custom.impl.ProgramRedisRepository; import org.openlmis.referencedata.repository.custom.impl.SupervisoryNodeDtoRedisRepository; import org.openlmis.referencedata.service.AuthenticationHelper; @@ -332,6 +333,9 @@ public abstract class BaseWebIntegrationTest { @MockBean protected DataImportService dataImportService; + @MockBean + protected WardRepository wardRepository; + /** * Constructor for test. */ diff --git a/src/integration-test/java/org/openlmis/referencedata/web/WardControllerIntegrationTest.java b/src/integration-test/java/org/openlmis/referencedata/web/WardControllerIntegrationTest.java new file mode 100644 index 00000000..6a4eec13 --- /dev/null +++ b/src/integration-test/java/org/openlmis/referencedata/web/WardControllerIntegrationTest.java @@ -0,0 +1,309 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org. + */ + +package org.openlmis.referencedata.web; + +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.openlmis.referencedata.domain.RightName.WARDS_MANAGE; + +import guru.nidi.ramltester.junit.RamlMatchers; +import java.util.Collections; +import java.util.Optional; +import java.util.UUID; +import org.apache.http.HttpStatus; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.openlmis.referencedata.domain.Facility; +import org.openlmis.referencedata.domain.Ward; +import org.openlmis.referencedata.dto.WardDto; +import org.openlmis.referencedata.testbuilder.FacilityDataBuilder; +import org.openlmis.referencedata.testbuilder.WardDataBuilder; +import org.openlmis.referencedata.util.messagekeys.WardMessageKeys; +import org.openlmis.referencedata.utils.AuditLogHelper; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +@SuppressWarnings("PMD.TooManyMethods") +public class WardControllerIntegrationTest extends BaseWebIntegrationTest { + + private static final String RESOURCE_URL = WardController.RESOURCE_PATH; + private static final String ID_URL = RESOURCE_URL + "/{id}"; + + private static final String NAME = "name"; + + private Facility facility = new FacilityDataBuilder().build(); + private final Ward ward = new WardDataBuilder().withFacility(facility).build(); + private final WardDto wardDto = WardDto.newInstance(ward); + + @Before + public void setUp() { + given(wardRepository.saveAndFlush(any(Ward.class))) + .willAnswer(new SaveAnswer<>()); + } + + @Test + public void shouldReturnPageOfWards() { + given(wardRepository.search(nullable(UUID.class), + nullable(Boolean.class), any(Pageable.class))) + .willReturn(new PageImpl<>(Collections.singletonList(ward))); + + restAssured.given() + .header(HttpHeaders.AUTHORIZATION, getTokenHeader()) + .queryParam("page", pageable.getPageNumber()) + .queryParam("size", pageable.getPageSize()) + .when() + .get(RESOURCE_URL) + .then() + .statusCode(HttpStatus.SC_OK) + .body("content", Matchers.hasSize(1)) + .body("content[0].id", Matchers.is(ward.getId().toString())) + .body("content[0].name", Matchers.is(ward.getName())); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldReturnUnauthorizedForAllWardsEndpointIfUserIsNotAuthorized() { + restAssured.given() + .when() + .get(RESOURCE_URL) + .then() + .statusCode(HttpStatus.SC_UNAUTHORIZED); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldCreateWard() { + mockUserHasRight(WARDS_MANAGE); + given(facilityRepository.findById(facility.getId())).willReturn(Optional.of(facility)); + + restAssured + .given() + .header(HttpHeaders.AUTHORIZATION, getTokenHeader()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .body(wardDto) + .when() + .post(RESOURCE_URL) + .then() + .statusCode(HttpStatus.SC_CREATED) + .body(ID, Matchers.is(Matchers.notNullValue())) + .body(NAME, Matchers.is(wardDto.getName())); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldReturnUnauthorizedForCreateWardEndpointIfUserIsNotAuthorized() { + restAssured + .given() + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .body(wardDto) + .when() + .post(RESOURCE_URL) + .then() + .statusCode(HttpStatus.SC_UNAUTHORIZED); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldReturnGivenWard() { + given(wardRepository.findById(wardDto.getId())) + .willReturn(Optional.of(ward)); + + restAssured + .given() + .header(HttpHeaders.AUTHORIZATION, getTokenHeader()) + .pathParam(ID, wardDto.getId().toString()) + .when() + .get(ID_URL) + .then() + .statusCode(HttpStatus.SC_OK) + .body(ID, Matchers.is(wardDto.getId().toString())) + .body(NAME, Matchers.is(wardDto.getName())); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldReturnNotFoundMessageIfWardDoesNotExistForGivenWardEndpoint() { + given(wardRepository.findById(wardDto.getId())).willReturn(Optional.empty()); + + restAssured + .given() + .header(HttpHeaders.AUTHORIZATION, getTokenHeader()) + .pathParam(ID, wardDto.getId().toString()) + .when() + .get(ID_URL) + .then() + .statusCode(HttpStatus.SC_NOT_FOUND) + .body(MESSAGE_KEY, Matchers.is(WardMessageKeys.ERROR_WARD_NOT_FOUND)); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldReturnUnauthorizedForGetWardEndpointIfUserIsNotAuthorized() { + restAssured + .given() + .pathParam(ID, wardDto.getId().toString()) + .when() + .get(ID_URL) + .then() + .statusCode(HttpStatus.SC_UNAUTHORIZED); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldUpdateWard() { + mockUserHasRight(WARDS_MANAGE); + given(wardRepository.findById(wardDto.getId())) + .willReturn(Optional.of(ward)); + given(facilityRepository.findById(facility.getId())) + .willReturn(Optional.of(facility)); + + restAssured + .given() + .header(HttpHeaders.AUTHORIZATION, getTokenHeader()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .pathParam(ID, wardDto.getId().toString()) + .body(wardDto) + .when() + .put(ID_URL) + .then() + .statusCode(HttpStatus.SC_OK) + .body(ID, Matchers.is(wardDto.getId().toString())) + .body(NAME, Matchers.is(wardDto.getName())); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldReturnBadRequestMessageIfWardCannotBeUpdated() { + mockUserHasRight(WARDS_MANAGE); + + restAssured + .given() + .header(HttpHeaders.AUTHORIZATION, getTokenHeader()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .pathParam(ID, UUID.randomUUID().toString()) + .body(wardDto) + .when() + .put(ID_URL) + .then() + .statusCode(HttpStatus.SC_BAD_REQUEST) + .body(MESSAGE_KEY, Matchers.is(WardMessageKeys.ERROR_WARD_ID_MISMATCH)); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldReturnUnauthorizedForUpdateWardEndpointIfUserIsNotAuthorized() { + restAssured + .given() + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .pathParam(ID, wardDto.getId().toString()) + .body(wardDto) + .when() + .put(ID_URL) + .then() + .statusCode(HttpStatus.SC_UNAUTHORIZED); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldDeleteWard() { + mockUserHasRight(WARDS_MANAGE); + given(wardRepository.existsById(wardDto.getId())).willReturn(true); + + restAssured + .given() + .header(HttpHeaders.AUTHORIZATION, getTokenHeader()) + .pathParam(ID, wardDto.getId().toString()) + .when() + .delete(ID_URL) + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldReturnNotFoundMessageIfWardDoesNotExistForDeleteWardEndpoint() { + mockUserHasRight(WARDS_MANAGE); + given(wardRepository.existsById(wardDto.getId())).willReturn(false); + + restAssured + .given() + .header(HttpHeaders.AUTHORIZATION, getTokenHeader()) + .pathParam(ID, wardDto.getId().toString()) + .when() + .delete(ID_URL) + .then() + .statusCode(HttpStatus.SC_NOT_FOUND) + .body(MESSAGE_KEY, Matchers.is(WardMessageKeys.ERROR_WARD_NOT_FOUND)); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldReturnUnauthorizedForDeleteWardEndpointIfUserIsNotAuthorized() { + restAssured + .given() + .pathParam(ID, wardDto.getId().toString()) + .when() + .delete(ID_URL) + .then() + .statusCode(HttpStatus.SC_UNAUTHORIZED); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void getAuditLogShouldReturnNotFoundIfEntityDoesNotExist() { + doNothing() + .when(rightService) + .checkAdminRight(WARDS_MANAGE); + given(wardRepository.findById(any(UUID.class))).willReturn(Optional.empty()); + + AuditLogHelper.notFound(restAssured, getTokenHeader(), RESOURCE_URL); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldGetAuditLog() { + doNothing() + .when(rightService) + .checkAdminRight(WARDS_MANAGE); + given(wardRepository.findById(any(UUID.class))).willReturn( + Optional.of(new WardDataBuilder().build())); + + AuditLogHelper.ok(restAssured, getTokenHeader(), RESOURCE_URL); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + +} diff --git a/src/main/java/org/openlmis/referencedata/domain/RightName.java b/src/main/java/org/openlmis/referencedata/domain/RightName.java index 4f5b0868..c9d3e214 100644 --- a/src/main/java/org/openlmis/referencedata/domain/RightName.java +++ b/src/main/java/org/openlmis/referencedata/domain/RightName.java @@ -40,6 +40,7 @@ public class RightName { public static final String ORDER_CREATE = "ORDER_CREATE"; public static final String DATA_EXPORT = "DATA_EXPORT"; public static final String DATA_IMPORT = "DATA_IMPORT"; + public static final String WARDS_MANAGE = "WARDS_MANAGE"; private RightName() { throw new UnsupportedOperationException(); diff --git a/src/main/java/org/openlmis/referencedata/domain/Ward.java b/src/main/java/org/openlmis/referencedata/domain/Ward.java new file mode 100644 index 00000000..827912e8 --- /dev/null +++ b/src/main/java/org/openlmis/referencedata/domain/Ward.java @@ -0,0 +1,163 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org. + */ + +package org.openlmis.referencedata.domain; + +import static org.apache.commons.lang3.BooleanUtils.isFalse; + +import java.util.Objects; +import java.util.Optional; +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name = "wards", schema = "referencedata") +@NoArgsConstructor +@AllArgsConstructor +public class Ward extends BaseEntity { + + @ManyToOne + @JoinColumn(name = "facilityId", nullable = false) + @Getter + @Setter + private Facility facility; + + @Column(nullable = false, columnDefinition = "text") + private String name; + + @Column(nullable = false, columnDefinition = "text") + private String description; + + @Column(nullable = false, columnDefinition = "boolean") + private boolean disabled; + + @Column(nullable = false, unique = true, columnDefinition = "text") + @Embedded + private Code code; + + /** + * Creates new ward object based on data from {@link Ward.Importer}. + * + * @param importer instance of {@link Ward.Importer} + * @return new instance of ward. + */ + public static Ward newWard(Importer importer) { + Ward ward = new Ward(); + if (importer.getFacility() != null) { + ward.setFacility(Facility.newFacility(importer.getFacility())); + } + ward.setId(importer.getId()); + ward.updateFrom(importer); + + return ward; + } + + /** + * Updates data based on data from {@link Ward.Importer}. + * + * @param importer instance of {@link Ward.Importer} + */ + public void updateFrom(Ward.Importer importer) { + name = importer.getName(); + description = importer.getDescription(); + disabled = importer.isDisabled(); + code = Code.code(importer.getCode()); + } + + /** + * Exports current state of ward object. + * + * @param exporter instance of {@link Ward.Exporter} + */ + public void export(Ward.Exporter exporter) { + exporter.setId(id); + exporter.setName(name); + exporter.setDescription(description); + exporter.setDisabled(disabled); + + String codeString = this.code.toString(); + if (isFalse(codeString.isEmpty())) { + exporter.setCode(codeString); + } + + Optional exporterOptional = + exporter.provideFacilityExporter(); + if (exporterOptional.isPresent()) { + Facility.Exporter facilityExporter = exporterOptional.get(); + facility.export(facilityExporter); + exporter.includeFacility(facilityExporter); + } + } + + /** + * Equal by a Ward's code. + * + * @param other the other Ward + * @return true if the two Ward's {@link Code} are equal. + */ + @Override + public boolean equals(Object other) { + if (!(other instanceof Ward)) { + return false; + } + + Ward otherWard = (Ward) other; + return code.equals(otherWard.code); + } + + @Override + public int hashCode() { + return Objects.hashCode(code); + } + + public interface Exporter extends BaseExporter { + void setName(String name); + + void setDescription(String description); + + void setDisabled(boolean disabled); + + void setCode(String code); + + Optional provideFacilityExporter(); + + void includeFacility(Facility.Exporter facilityExporter); + + } + + public interface Importer extends BaseImporter { + String getName(); + + String getDescription(); + + boolean isDisabled(); + + String getCode(); + + Facility.Importer getFacility(); + + } + +} diff --git a/src/main/java/org/openlmis/referencedata/dto/WardDto.java b/src/main/java/org/openlmis/referencedata/dto/WardDto.java new file mode 100644 index 00000000..86d09cbe --- /dev/null +++ b/src/main/java/org/openlmis/referencedata/dto/WardDto.java @@ -0,0 +1,62 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org. + */ + +package org.openlmis.referencedata.dto; + +import java.util.Optional; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.openlmis.referencedata.domain.Facility; +import org.openlmis.referencedata.domain.Ward; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class WardDto extends BaseDto implements Ward.Exporter, Ward.Importer { + + private FacilityDto facility; + private String name; + private String description; + private boolean disabled; + private String code; + + /** + * Creates new instance based on domain object. + */ + public static WardDto newInstance(Ward ward) { + WardDto dto = new WardDto(); + ward.export(dto); + + return dto; + } + + @Override + public Optional provideFacilityExporter() { + return Optional.of(new FacilityDto()); + } + + @Override + public void includeFacility(Facility.Exporter facilityExporter) { + facility = (FacilityDto) facilityExporter; + } + +} \ No newline at end of file diff --git a/src/main/java/org/openlmis/referencedata/errorhandling/RefDataErrorHandling.java b/src/main/java/org/openlmis/referencedata/errorhandling/RefDataErrorHandling.java index 0c5b5aa0..386b7ebb 100644 --- a/src/main/java/org/openlmis/referencedata/errorhandling/RefDataErrorHandling.java +++ b/src/main/java/org/openlmis/referencedata/errorhandling/RefDataErrorHandling.java @@ -36,6 +36,7 @@ import org.openlmis.referencedata.util.messagekeys.SupplyLineMessageKeys; import org.openlmis.referencedata.util.messagekeys.SupplyPartnerMessageKeys; import org.openlmis.referencedata.util.messagekeys.TradeItemMessageKeys; +import org.openlmis.referencedata.util.messagekeys.WardMessageKeys; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataIntegrityViolationException; @@ -73,6 +74,7 @@ public class RefDataErrorHandling extends BaseHandler { SupplyPartnerMessageKeys.ERROR_ASSOCIATION_DUPLICATED); CONSTRAINT_MAP.put("unq_role_name", RoleMessageKeys.ERROR_MUST_HAVE_A_UNIQUE_NAME); CONSTRAINT_MAP.put("unq_ftap", FacilityTypeApprovedProductMessageKeys.ERROR_DUPLICATED); + CONSTRAINT_MAP.put("unq_ward_code", WardMessageKeys.ERROR_CODE_DUPLICATED); // https://www.postgresql.org/docs/9.6/static/errcodes-appendix.html SQL_STATES.put("23503", OrderableMessageKeys.ERROR_NOT_FOUND); diff --git a/src/main/java/org/openlmis/referencedata/repository/WardRepository.java b/src/main/java/org/openlmis/referencedata/repository/WardRepository.java new file mode 100644 index 00000000..375c277b --- /dev/null +++ b/src/main/java/org/openlmis/referencedata/repository/WardRepository.java @@ -0,0 +1,49 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org. + */ + +package org.openlmis.referencedata.repository; + +import java.util.UUID; +import org.javers.spring.annotation.JaversSpringDataAuditable; +import org.openlmis.referencedata.domain.Ward; +import org.openlmis.referencedata.repository.custom.WardRepositoryCustom; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +@JaversSpringDataAuditable +public interface WardRepository extends JpaRepository, + BaseAuditableRepository, WardRepositoryCustom { + + @Query(value = "SELECT\n" + + " w.*\n" + + "FROM\n" + + " referencedata.wards w\n" + + "WHERE\n" + + " id NOT IN (\n" + + " SELECT\n" + + " id\n" + + " FROM\n" + + " referencedata.wards w\n" + + " INNER JOIN referencedata.jv_global_id g " + + "ON CAST(w.id AS varchar) = SUBSTRING(g.local_id, 2, 36)\n" + + " INNER JOIN referencedata.jv_snapshot s ON g.global_id_pk = s.global_id_fk\n" + + " )\n" + + " ", + nativeQuery = true) + Page findAllWithoutSnapshots(Pageable pageable); + +} diff --git a/src/main/java/org/openlmis/referencedata/repository/custom/WardRepositoryCustom.java b/src/main/java/org/openlmis/referencedata/repository/custom/WardRepositoryCustom.java new file mode 100644 index 00000000..0758576c --- /dev/null +++ b/src/main/java/org/openlmis/referencedata/repository/custom/WardRepositoryCustom.java @@ -0,0 +1,27 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org. + */ + +package org.openlmis.referencedata.repository.custom; + +import java.util.UUID; +import org.openlmis.referencedata.domain.Ward; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface WardRepositoryCustom { + + Page search(UUID facilityId, Boolean disabled, Pageable pageable); + +} diff --git a/src/main/java/org/openlmis/referencedata/repository/custom/impl/WardRepositoryCustomImpl.java b/src/main/java/org/openlmis/referencedata/repository/custom/impl/WardRepositoryCustomImpl.java new file mode 100644 index 00000000..c6433f2e --- /dev/null +++ b/src/main/java/org/openlmis/referencedata/repository/custom/impl/WardRepositoryCustomImpl.java @@ -0,0 +1,133 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org. + */ + +package org.openlmis.referencedata.repository.custom.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Order; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import org.apache.commons.lang3.tuple.Pair; +import org.openlmis.referencedata.domain.Facility; +import org.openlmis.referencedata.domain.Ward; +import org.openlmis.referencedata.repository.custom.WardRepositoryCustom; +import org.openlmis.referencedata.util.Pagination; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +public class WardRepositoryCustomImpl implements WardRepositoryCustom { + + protected static final String FACILITY = "facility"; + protected static final String DISABLED = "disabled"; + public static final String ID = "id"; + + @PersistenceContext + private EntityManager entityManager; + + @Override + public Page search(UUID facilityId, Boolean disabled, Pageable pageable) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery countQuery = builder.createQuery(Long.class); + countQuery = prepareQuery(facilityId, disabled, countQuery, true, pageable); + Long count = entityManager.createQuery(countQuery).getSingleResult(); + + if (count == 0) { + return Pagination.getPage(Collections.emptyList(), pageable, 0); + } + + CriteriaQuery query = builder.createQuery(Ward.class); + query = prepareQuery(facilityId, disabled, query, false, pageable); + Pair maxAndFirst = PageableUtil.querysMaxAndFirstResult(pageable); + + List result = entityManager + .createQuery(query) + .setMaxResults(maxAndFirst.getLeft()) + .setFirstResult(maxAndFirst.getRight()) + .getResultList(); + + return Pagination.getPage(result, pageable, count); + } + + private CriteriaQuery prepareQuery(UUID facilityId, Boolean disabled, + CriteriaQuery query, boolean count, Pageable pageable) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + Root root = query.from(Ward.class); + + Join facilityJoin = root.join(FACILITY); + + Predicate conjunction = builder.conjunction(); + + if (count) { + CriteriaQuery countQuery = (CriteriaQuery) query; + query = (CriteriaQuery) countQuery.select(builder.count(root)); + } + + if (facilityId != null) { + conjunction = builder.and(conjunction, builder.equal(facilityJoin.get(ID), facilityId)); + } + conjunction = addEqualsFilter(conjunction, builder, root, DISABLED, disabled); + + query.where(conjunction); + + if (!count && pageable != null && pageable.getSort() != null) { + query = addSortProperties(query, root, pageable); + } + + return query; + + } + + private Predicate addEqualsFilter(Predicate predicate, CriteriaBuilder builder, Root root, + String filterKey, Object filterValue) { + if (filterValue != null) { + return builder.and( + predicate, + builder.equal( + root.get(filterKey), filterValue)); + } else { + return predicate; + } + } + + private CriteriaQuery addSortProperties(CriteriaQuery query, + Root root, Pageable pageable) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + List orders = new ArrayList<>(); + Iterator iterator = pageable.getSort().iterator(); + + Sort.Order order; + while (iterator.hasNext()) { + order = iterator.next(); + if (order.isAscending()) { + orders.add(builder.asc(root.get(order.getProperty()))); + } else { + orders.add(builder.desc(root.get(order.getProperty()))); + } + } + return query.orderBy(orders); + } + +} diff --git a/src/main/java/org/openlmis/referencedata/util/messagekeys/MessageKeys.java b/src/main/java/org/openlmis/referencedata/util/messagekeys/MessageKeys.java index 8a43e3e2..b1543a8e 100644 --- a/src/main/java/org/openlmis/referencedata/util/messagekeys/MessageKeys.java +++ b/src/main/java/org/openlmis/referencedata/util/messagekeys/MessageKeys.java @@ -127,6 +127,7 @@ public abstract class MessageKeys { protected static final String EXTENSION = "extension"; protected static final String TOO = "too"; protected static final String LARGE = "large"; + protected static final String WARD = "ward"; // Common to subclasses protected static final String EMAIL = "email"; diff --git a/src/main/java/org/openlmis/referencedata/util/messagekeys/WardMessageKeys.java b/src/main/java/org/openlmis/referencedata/util/messagekeys/WardMessageKeys.java new file mode 100644 index 00000000..71b4d91a --- /dev/null +++ b/src/main/java/org/openlmis/referencedata/util/messagekeys/WardMessageKeys.java @@ -0,0 +1,26 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org. + */ + +package org.openlmis.referencedata.util.messagekeys; + +public class WardMessageKeys extends MessageKeys { + + private static final String ERROR = join(SERVICE_ERROR, WARD); + + public static final String ERROR_WARD_ID_MISMATCH = join(ERROR, ID_MISMATCH); + public static final String ERROR_WARD_NOT_FOUND = join(ERROR, NOT_FOUND); + public static final String ERROR_CODE_DUPLICATED = join(ERROR, CODE, DUPLICATED); + +} diff --git a/src/main/java/org/openlmis/referencedata/web/WardController.java b/src/main/java/org/openlmis/referencedata/web/WardController.java new file mode 100644 index 00000000..a86e19ff --- /dev/null +++ b/src/main/java/org/openlmis/referencedata/web/WardController.java @@ -0,0 +1,212 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org. + */ + +package org.openlmis.referencedata.web; + +import static org.openlmis.referencedata.domain.RightName.WARDS_MANAGE; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import org.openlmis.referencedata.domain.Facility; +import org.openlmis.referencedata.domain.Ward; +import org.openlmis.referencedata.dto.WardDto; +import org.openlmis.referencedata.exception.NotFoundException; +import org.openlmis.referencedata.exception.ValidationMessageException; +import org.openlmis.referencedata.repository.FacilityRepository; +import org.openlmis.referencedata.repository.WardRepository; +import org.openlmis.referencedata.util.Message; +import org.openlmis.referencedata.util.Pagination; +import org.openlmis.referencedata.util.messagekeys.FacilityMessageKeys; +import org.openlmis.referencedata.util.messagekeys.WardMessageKeys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Controller +@RequestMapping(WardController.RESOURCE_PATH) +@Transactional +public class WardController extends BaseController { + + private static final Logger LOGGER = LoggerFactory.getLogger(WardController.class); + + public static final String RESOURCE_PATH = API_PATH + "/wards"; + + @Autowired + private WardRepository wardRepository; + + @Autowired + private FacilityRepository facilityRepository; + + /** + * Allows the creation of a new ward. If the id is specified, it will be ignored. + */ + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @ResponseBody + public WardDto createWard(@RequestBody WardDto ward) { + rightService.checkAdminRight(WARDS_MANAGE); + LOGGER.debug("Creating new ward"); + Ward newWard = Ward.newWard(ward); + + LOGGER.debug("Find facility"); + Facility facility = findFacility(ward.getFacility().getId()); + newWard.setFacility(facility); + + newWard.setId(null); + newWard = wardRepository.saveAndFlush(newWard); + + return WardDto.newInstance(newWard); + } + + /** + * Updates the specified ward. + */ + @PutMapping(value = "/{id}") + @ResponseStatus(HttpStatus.OK) + @ResponseBody + public WardDto saveWard(@PathVariable("id") UUID id, + @RequestBody WardDto ward) { + rightService.checkAdminRight(WARDS_MANAGE); + if (null != ward.getId() && !Objects.equals(ward.getId(), id)) { + throw new ValidationMessageException(WardMessageKeys.ERROR_WARD_ID_MISMATCH); + } + + LOGGER.debug("Updating ward"); + Ward db; + Optional wardOptional = wardRepository.findById(id); + if (wardOptional.isPresent()) { + db = wardOptional.get(); + db.updateFrom(ward); + } else { + db = Ward.newWard(ward); + db.setId(id); + } + + LOGGER.debug("Find facility"); + Facility facility = findFacility(ward.getFacility().getId()); + db.setFacility(facility); + + wardRepository.saveAndFlush(db); + + return WardDto.newInstance(db); + } + + /** + * Deletes the specified ward. + */ + @DeleteMapping(value = "/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteWard(@PathVariable("id") UUID id) { + rightService.checkAdminRight(WARDS_MANAGE); + if (!wardRepository.existsById(id)) { + throw new NotFoundException(WardMessageKeys.ERROR_WARD_NOT_FOUND); + } + + wardRepository.deleteById(id); + } + + /** + * Retrieves all wards. Note that an empty collection rather than a 404 should be + * returned if no wards exist. + */ + @GetMapping + @ResponseStatus(HttpStatus.OK) + @ResponseBody + public Page getAllWards( + @RequestParam(value = "facilityId", required = false) UUID facilityId, + @RequestParam(value = "disabled", required = false) Boolean disabled, + Pageable pageable) { + Page page = wardRepository.search(facilityId, disabled, pageable); + List content = page + .getContent() + .stream() + .map(WardDto::newInstance) + .collect(Collectors.toList()); + return Pagination.getPage(content, pageable, page.getTotalElements()); + } + + /** + * Retrieves the specified ward. + */ + @GetMapping(value = "/{id}") + @ResponseStatus(HttpStatus.OK) + @ResponseBody + public WardDto getSpecifiedWard(@PathVariable("id") UUID id) { + Ward ward = wardRepository.findById(id) + .orElseThrow(() -> new NotFoundException(WardMessageKeys.ERROR_WARD_NOT_FOUND)); + + return WardDto.newInstance(ward); + } + + /** + * Retrieves audit information related to the specified ward. + * + * @param author The author of the changes which should be returned. + * If null or empty, changes are returned regardless of author. + * @param changedPropertyName The name of the property about which changes should be returned. + * If null or empty, changes associated with any and all properties are returned. + * @param page A Pageable object that allows client to optionally add "page" (page number) + * and "size" (page size) query parameters to the request. + */ + @GetMapping(value = "/{id}/auditLog") + @ResponseStatus(HttpStatus.OK) + @ResponseBody + public ResponseEntity getWardAuditLog( + @PathVariable("id") UUID id, + @RequestParam(name = "author", required = false, defaultValue = "") String author, + @RequestParam(name = "changedPropertyName", required = false, defaultValue = "") + String changedPropertyName, + //Because JSON is all we formally support, returnJSON is excluded from our JavaDoc + @RequestParam(name = "returnJSON", required = false, defaultValue = "true") + boolean returnJson, + Pageable page) { + rightService.checkAdminRight(WARDS_MANAGE); + + //Return a 404 if the specified instance can't be found + Ward instance = wardRepository.findById(id).orElse(null); + if (instance == null) { + throw new NotFoundException(WardMessageKeys.ERROR_WARD_NOT_FOUND); + } + + return getAuditLogResponse(Ward.class, id, author, changedPropertyName, page, + returnJson); + } + + private Facility findFacility(UUID facilityId) { + return facilityRepository.findById(facilityId) + .orElseThrow(() -> new ValidationMessageException( + new Message(FacilityMessageKeys.ERROR_NOT_FOUND_WITH_ID, facilityId))); + } + +} diff --git a/src/main/resources/api-definition.yaml b/src/main/resources/api-definition.yaml index 28209dd0..ed64926c 100644 --- a/src/main/resources/api-definition.yaml +++ b/src/main/resources/api-definition.yaml @@ -116,6 +116,10 @@ schemas: - tradeItemPage: !include schemas/tradeItemPage.json + - ward: !include schemas/ward.json + + - wardPage: !include schemas/wardPage.json + - orderable: !include schemas/orderable.json - orderableChildDto: !include schemas/orderableChildDto.json @@ -933,6 +937,140 @@ resourceTypes: /{id}/auditLog: type: instanceAuditLog + /wards: + displayName: wards + get: + is: [ secured, paginated, sorted ] + description: Get all wards that match the given parameters. + responses: + 200: + headers: + Keep-Alive: + body: + application/json: + schema: wardPage + 401: + headers: + Keep-Alive: + body: + application/json: + 403: + body: + application/json: + schema: localizedErrorResponse + post: + is: [ secured ] + description: Creates given ward if possible. + body: + application/json: + schema: ward + responses: + 201: + headers: + Keep-Alive: + body: + application/json: + schema: ward + 400: + body: + application/json: + schema: localizedErrorResponse + 401: + headers: + Keep-Alive: + body: + application/json: + 403: + body: + application/json: + schema: localizedErrorResponse + /{id}: + uriParameters: + id: + displayName: id + type: string + required: true + repeat: false + get: + is: [ secured ] + description: Get chosen ward. + responses: + 200: + headers: + Keep-Alive: + body: + application/json: + schema: ward + 404: + headers: + Keep-Alive: + body: + application/json: + schema: localizedErrorResponse + 401: + headers: + Keep-Alive: + body: + application/json: + 403: + body: + application/json: + schema: localizedErrorResponse + put: + is: [ secured ] + description: Update existing ward. + body: + application/json: + schema: ward + responses: + 200: + headers: + Keep-Alive: + body: + application/json: + schema: ward + 400: + body: + application/json: + schema: localizedErrorResponse + 401: + headers: + Keep-Alive: + body: + application/json: + 403: + body: + application/json: + schema: localizedErrorResponse + 404: + body: + application/json: + schema: localizedErrorResponse + delete: + is: [ secured ] + description: "Completely removes ward. This action is not recoverable." + responses: + 204: + headers: + Keep-Alive: + 400: + body: + application/json: + schema: localizedErrorResponse + 401: + headers: + Keep-Alive: + body: + application/json: + 404: + headers: + Keep-Alive: + body: + application/json: + schema: localizedErrorResponse + /{id}/auditLog: + type: instanceAuditLog + /commodityTypes: displayName: Commodity Type put: diff --git a/src/main/resources/db/migration/20240410074547750__create_wards_table.sql b/src/main/resources/db/migration/20240410074547750__create_wards_table.sql new file mode 100644 index 00000000..c6b4dbc5 --- /dev/null +++ b/src/main/resources/db/migration/20240410074547750__create_wards_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE wards ( + id UUID PRIMARY KEY, + facilityid UUID NOT NULL, + name text, + description text, + code text NOT NULL, + disabled boolean NOT NULL, + CONSTRAINT ward_facility_fk FOREIGN KEY (facilityid) REFERENCES facilities(id), + CONSTRAINT unq_ward_code UNIQUE (code) +); \ No newline at end of file diff --git a/src/main/resources/db/migration/20240415084147861__add_wards_manage_right.sql b/src/main/resources/db/migration/20240415084147861__add_wards_manage_right.sql new file mode 100644 index 00000000..e4a81879 --- /dev/null +++ b/src/main/resources/db/migration/20240415084147861__add_wards_manage_right.sql @@ -0,0 +1 @@ +INSERT INTO rights (id, description, name, type) VALUES ('5445e435-11f6-4bda-b28b-b18b35f1bd47', NULL, 'WARDS_MANAGE', 'GENERAL_ADMIN'); \ No newline at end of file diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index b6f4b406..78189f12 100755 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -281,6 +281,10 @@ referenceData.error.dataExport.missing.format.parameter = The format parameter i referenceData.error.dataExport.missing.data.parameter = The data parameter is missing. referenceData.error.dataExport.lacksParameters = To export data, you need to provide two parameters: format and data. +referenceData.error.ward.idMismatch=Ward ID mismatch. The ID that was provided in the ward body differs from the one in url. +referenceData.error.ward.notFound=Ward not found. +referenceData.error.ward.code.duplicated=Ward with this code already exists. + # System messages referenceData.error.unauthorized=You do not have the following right to perform this action: {0} referenceData.error.unauthorized.generic=You do not have rights to perform this action diff --git a/src/main/resources/schemas/ward.json b/src/main/resources/schemas/ward.json new file mode 100644 index 00000000..48afd6fb --- /dev/null +++ b/src/main/resources/schemas/ward.json @@ -0,0 +1,35 @@ +{ + "type": "object", + "$schema": "http://json-schema.org/draft-04/schema", + "title": "Ward", + "description": "A single ward", + "properties": { + "id": { + "type": "string", + "title": "id" + }, + "code": { + "type": "string", + "title": "code" + }, + "name": { + "type": "string", + "title": "name" + }, + "description": { + "type": "string", + "title": "description" + }, + "disabled": { + "type": "boolean", + "title": "disabled" + }, + "facility": { + "type": "object", + "$ref": "facility.json" + } + }, + "required": [ + "code" + ] +} \ No newline at end of file diff --git a/src/main/resources/schemas/wardPage.json b/src/main/resources/schemas/wardPage.json new file mode 100644 index 00000000..01c7adbb --- /dev/null +++ b/src/main/resources/schemas/wardPage.json @@ -0,0 +1,60 @@ +{ + "type": "object", + "$schema": "http://json-schema.org/draft-04/schema", + "title": "Collection", + "description": "Paginated collection", + "properties": { + "content": { + "type": "array", + "items": { + "type": "object", + "$ref": "ward.json" + } + }, + "totalPages": { + "type": "integer", + "title": "totalPages" + }, + "totalElements": { + "type": "integer", + "title": "totalElements" + }, + "size": { + "type": "integer", + "title": "size" + }, + "number": { + "type": "integer", + "title": "number" + }, + "numberOfElements": { + "type": "integer", + "title": "numberOfElements" + }, + "last": { + "type": "boolean", + "title": "last" + }, + "first": { + "type": "boolean", + "title": "first" + }, + "sort?": { + "title": "sort", + "type": "array", + "items": { + "type": "object" + } + } + }, + "required": [ + "content", + "totalPages", + "totalElements", + "size", + "number", + "numberOfElements", + "first", + "last" + ] +} diff --git a/src/test/java/org/openlmis/referencedata/domain/WardTest.java b/src/test/java/org/openlmis/referencedata/domain/WardTest.java new file mode 100644 index 00000000..9ceaa3dc --- /dev/null +++ b/src/test/java/org/openlmis/referencedata/domain/WardTest.java @@ -0,0 +1,62 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org. + */ + +package org.openlmis.referencedata.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.openlmis.referencedata.dto.WardDto; +import org.openlmis.referencedata.testbuilder.WardDataBuilder; + +public class WardTest { + + @Test + public void shouldBeEqualByCode() { + String testCodeString = "test_code"; + Ward ward = new WardDataBuilder().withCode(testCodeString).build(); + Ward wardDupl = new WardDataBuilder().withCode(testCodeString).build(); + + assertTrue(ward.equals(wardDupl)); + assertTrue(wardDupl.equals(ward)); + } + + @Test + public void shouldCreateNewInstance() { + Ward importerAsDomain = new WardDataBuilder().build(); + WardDto importer = new WardDto(); + importerAsDomain.export(importer); + + Ward newInstance = Ward.newWard(importer); + assertThat(newInstance).isEqualTo(importerAsDomain); + } + + @Test + public void shouldExportData() { + Ward ward = new WardDataBuilder().build(); + WardDto dto = new WardDto(); + + ward.export(dto); + + assertThat(dto.getId()).isEqualTo(ward.getId()); + assertThat(dto.getName()).isEqualTo(ward.getName()); + assertThat(dto.isDisabled()).isEqualTo(ward.isDisabled()); + assertThat(dto.getDescription()).isEqualTo(ward.getDescription()); + assertThat(dto.getCode()).isEqualTo(ward.getCode().toString()); + assertThat(dto.getFacility().getCode()).isEqualTo(ward.getFacility().getCode()); + } + +} diff --git a/src/test/java/org/openlmis/referencedata/dto/WardDtoTest.java b/src/test/java/org/openlmis/referencedata/dto/WardDtoTest.java new file mode 100644 index 00000000..4a287b93 --- /dev/null +++ b/src/test/java/org/openlmis/referencedata/dto/WardDtoTest.java @@ -0,0 +1,29 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org. + */ + +package org.openlmis.referencedata.dto; + +import org.junit.Test; +import org.openlmis.referencedata.ToStringTestUtils; + +public class WardDtoTest { + + @Test + public void shouldImplementToString() { + WardDto dto = new WardDto(); + ToStringTestUtils.verify(WardDto.class, dto); + } + +} diff --git a/src/test/java/org/openlmis/referencedata/testbuilder/WardDataBuilder.java b/src/test/java/org/openlmis/referencedata/testbuilder/WardDataBuilder.java new file mode 100644 index 00000000..dac8cf29 --- /dev/null +++ b/src/test/java/org/openlmis/referencedata/testbuilder/WardDataBuilder.java @@ -0,0 +1,117 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org. + */ + +package org.openlmis.referencedata.testbuilder; + +import java.util.UUID; +import org.openlmis.referencedata.domain.Code; +import org.openlmis.referencedata.domain.Facility; +import org.openlmis.referencedata.domain.Ward; + +public class WardDataBuilder { + + private static int instanceNumber = 0; + + private Facility facility; + private UUID id; + private String name; + private String description; + private boolean disabled; + private Code code; + + /** + * Builds instance of {@link WardDataBuilder} with sample data. + */ + public WardDataBuilder() { + id = UUID.randomUUID(); + name = "Ward " + instanceNumber; + description = "Test ward"; + disabled = false; + code = Code.code("W" + instanceNumber); + facility = new FacilityDataBuilder().build(); + } + + /** + * Builds instance of {@link Ward}. + */ + public Ward buildAsNew() { + Ward ward = new Ward(facility, name, description, disabled, code); + + return ward; + } + + /** + * Builds instance of {@link Ward}. + */ + public Ward build() { + Ward ward = buildAsNew(); + ward.setId(id); + + return ward; + } + + /** + * Sets id for new {@link Ward}. + */ + public WardDataBuilder withId(UUID id) { + this.id = id; + return this; + } + + /** + * Sets null id for new {@link Ward}. + */ + public WardDataBuilder withoutId() { + return withId(null); + } + + public WardDataBuilder withCode(String code) { + this.code = Code.code(code); + return this; + } + + /** + * Sets name for new {@link Ward}. + */ + public WardDataBuilder withName(String name) { + this.name = name; + return this; + } + + /** + * Sets description for new {@link Ward}. + */ + public WardDataBuilder withDescription(String description) { + this.description = description; + return this; + } + + /** + * Sets disabled flag for new {@link Ward}. + */ + public WardDataBuilder withDisabledFlag(boolean disabled) { + this.disabled = disabled; + return this; + } + + /** + * Sets facility for new {@link Ward}. + */ + public WardDataBuilder withFacility(Facility facility) { + this.facility = facility; + return this; + } + +} From da608df6d7bc89158014810be4b2cb32673d327b Mon Sep 17 00:00:00 2001 From: Szymon Radziszewski Date: Thu, 18 Apr 2024 15:53:20 +0200 Subject: [PATCH 2/2] OAM-46: Added PUT /api/wards/saveAll endpoint --- .../web/WardControllerIntegrationTest.java | 47 ++++++++--- .../referencedata/web/WardController.java | 82 +++++++++++++------ src/main/resources/api-definition.yaml | 48 +++++++++++ 3 files changed, 140 insertions(+), 37 deletions(-) diff --git a/src/integration-test/java/org/openlmis/referencedata/web/WardControllerIntegrationTest.java b/src/integration-test/java/org/openlmis/referencedata/web/WardControllerIntegrationTest.java index 6a4eec13..37823ec3 100644 --- a/src/integration-test/java/org/openlmis/referencedata/web/WardControllerIntegrationTest.java +++ b/src/integration-test/java/org/openlmis/referencedata/web/WardControllerIntegrationTest.java @@ -15,6 +15,7 @@ package org.openlmis.referencedata.web; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.nullable; @@ -47,6 +48,7 @@ public class WardControllerIntegrationTest extends BaseWebIntegrationTest { private static final String RESOURCE_URL = WardController.RESOURCE_PATH; private static final String ID_URL = RESOURCE_URL + "/{id}"; + private static final String SAVE_ALL_URL = RESOURCE_URL + "/saveAll"; private static final String NAME = "name"; @@ -75,8 +77,8 @@ public void shouldReturnPageOfWards() { .then() .statusCode(HttpStatus.SC_OK) .body("content", Matchers.hasSize(1)) - .body("content[0].id", Matchers.is(ward.getId().toString())) - .body("content[0].name", Matchers.is(ward.getName())); + .body("content[0].id", is(ward.getId().toString())) + .body("content[0].name", is(ward.getName())); assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); } @@ -106,8 +108,8 @@ public void shouldCreateWard() { .post(RESOURCE_URL) .then() .statusCode(HttpStatus.SC_CREATED) - .body(ID, Matchers.is(Matchers.notNullValue())) - .body(NAME, Matchers.is(wardDto.getName())); + .body(ID, is(Matchers.notNullValue())) + .body(NAME, is(wardDto.getName())); assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); } @@ -139,8 +141,8 @@ public void shouldReturnGivenWard() { .get(ID_URL) .then() .statusCode(HttpStatus.SC_OK) - .body(ID, Matchers.is(wardDto.getId().toString())) - .body(NAME, Matchers.is(wardDto.getName())); + .body(ID, is(wardDto.getId().toString())) + .body(NAME, is(wardDto.getName())); assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); } @@ -157,7 +159,7 @@ public void shouldReturnNotFoundMessageIfWardDoesNotExistForGivenWardEndpoint() .get(ID_URL) .then() .statusCode(HttpStatus.SC_NOT_FOUND) - .body(MESSAGE_KEY, Matchers.is(WardMessageKeys.ERROR_WARD_NOT_FOUND)); + .body(MESSAGE_KEY, is(WardMessageKeys.ERROR_WARD_NOT_FOUND)); assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); } @@ -193,8 +195,31 @@ public void shouldUpdateWard() { .put(ID_URL) .then() .statusCode(HttpStatus.SC_OK) - .body(ID, Matchers.is(wardDto.getId().toString())) - .body(NAME, Matchers.is(wardDto.getName())); + .body(ID, is(wardDto.getId().toString())) + .body(NAME, is(wardDto.getName())); + + assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); + } + + @Test + public void shouldUpdateListOfWards() { + mockUserHasRight(WARDS_MANAGE); + given(wardRepository.findById(wardDto.getId())) + .willReturn(Optional.of(ward)); + given(facilityRepository.findById(facility.getId())) + .willReturn(Optional.of(facility)); + + restAssured + .given() + .header(HttpHeaders.AUTHORIZATION, getTokenHeader()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .body(Collections.singletonList(wardDto)) + .when() + .put(SAVE_ALL_URL) + .then() + .statusCode(HttpStatus.SC_OK) + .assertThat() + .body("size()", is(1)); assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); } @@ -213,7 +238,7 @@ public void shouldReturnBadRequestMessageIfWardCannotBeUpdated() { .put(ID_URL) .then() .statusCode(HttpStatus.SC_BAD_REQUEST) - .body(MESSAGE_KEY, Matchers.is(WardMessageKeys.ERROR_WARD_ID_MISMATCH)); + .body(MESSAGE_KEY, is(WardMessageKeys.ERROR_WARD_ID_MISMATCH)); assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); } @@ -263,7 +288,7 @@ public void shouldReturnNotFoundMessageIfWardDoesNotExistForDeleteWardEndpoint() .delete(ID_URL) .then() .statusCode(HttpStatus.SC_NOT_FOUND) - .body(MESSAGE_KEY, Matchers.is(WardMessageKeys.ERROR_WARD_NOT_FOUND)); + .body(MESSAGE_KEY, is(WardMessageKeys.ERROR_WARD_NOT_FOUND)); assertThat(RAML_ASSERT_MESSAGE, restAssured.getLastReport(), RamlMatchers.hasNoViolations()); } diff --git a/src/main/java/org/openlmis/referencedata/web/WardController.java b/src/main/java/org/openlmis/referencedata/web/WardController.java index a86e19ff..a331ca3c 100644 --- a/src/main/java/org/openlmis/referencedata/web/WardController.java +++ b/src/main/java/org/openlmis/referencedata/web/WardController.java @@ -17,6 +17,7 @@ import static org.openlmis.referencedata.domain.RightName.WARDS_MANAGE; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -76,17 +77,28 @@ public class WardController extends BaseController { @ResponseBody public WardDto createWard(@RequestBody WardDto ward) { rightService.checkAdminRight(WARDS_MANAGE); - LOGGER.debug("Creating new ward"); - Ward newWard = Ward.newWard(ward); + return createOne(ward); + } - LOGGER.debug("Find facility"); - Facility facility = findFacility(ward.getFacility().getId()); - newWard.setFacility(facility); + /** + * Updates the list of wards. + */ + @PutMapping(value = "/saveAll") + @ResponseStatus(HttpStatus.OK) + @ResponseBody + public List saveWards(@RequestBody List wards) { + rightService.checkAdminRight(WARDS_MANAGE); - newWard.setId(null); - newWard = wardRepository.saveAndFlush(newWard); + List savedWards = new ArrayList<>(); + for (WardDto ward : wards) { + if (ward.getId() != null) { + savedWards.add(saveOne(ward.getId(), ward)); + } else { + savedWards.add(createOne(ward)); + } + } - return WardDto.newInstance(newWard); + return savedWards; } /** @@ -102,24 +114,7 @@ public WardDto saveWard(@PathVariable("id") UUID id, throw new ValidationMessageException(WardMessageKeys.ERROR_WARD_ID_MISMATCH); } - LOGGER.debug("Updating ward"); - Ward db; - Optional wardOptional = wardRepository.findById(id); - if (wardOptional.isPresent()) { - db = wardOptional.get(); - db.updateFrom(ward); - } else { - db = Ward.newWard(ward); - db.setId(id); - } - - LOGGER.debug("Find facility"); - Facility facility = findFacility(ward.getFacility().getId()); - db.setFacility(facility); - - wardRepository.saveAndFlush(db); - - return WardDto.newInstance(db); + return saveOne(id, ward); } /** @@ -203,6 +198,41 @@ public ResponseEntity getWardAuditLog( returnJson); } + private WardDto saveOne(UUID id, WardDto ward) { + LOGGER.debug("Updating ward"); + Ward db; + Optional wardOptional = wardRepository.findById(id); + if (wardOptional.isPresent()) { + db = wardOptional.get(); + db.updateFrom(ward); + } else { + db = Ward.newWard(ward); + db.setId(id); + } + + LOGGER.debug("Find facility"); + Facility facility = findFacility(ward.getFacility().getId()); + db.setFacility(facility); + + wardRepository.saveAndFlush(db); + + return WardDto.newInstance(db); + } + + private WardDto createOne(WardDto ward) { + LOGGER.debug("Creating new ward"); + Ward newWard = Ward.newWard(ward); + + LOGGER.debug("Find facility"); + Facility facility = findFacility(ward.getFacility().getId()); + newWard.setFacility(facility); + + newWard.setId(null); + newWard = wardRepository.saveAndFlush(newWard); + + return WardDto.newInstance(newWard); + } + private Facility findFacility(UUID facilityId) { return facilityRepository.findById(facilityId) .orElseThrow(() -> new ValidationMessageException( diff --git a/src/main/resources/api-definition.yaml b/src/main/resources/api-definition.yaml index ed64926c..bbdf208b 100644 --- a/src/main/resources/api-definition.yaml +++ b/src/main/resources/api-definition.yaml @@ -120,6 +120,12 @@ schemas: - wardPage: !include schemas/wardPage.json + - wardArray: | + { + "type": "array", + "items": { "type": "object", "$ref": "schemas/ward.json" } + } + - orderable: !include schemas/orderable.json - orderableChildDto: !include schemas/orderableChildDto.json @@ -942,6 +948,17 @@ resourceTypes: get: is: [ secured, paginated, sorted ] description: Get all wards that match the given parameters. + queryParameters: + facilityId: + displayName: facilityId + type: string + required: false + repeat: false + disabled: + displayName: disabled + type: boolean + required: false + repeat: false responses: 200: headers: @@ -984,6 +1001,37 @@ resourceTypes: body: application/json: schema: localizedErrorResponse + /saveAll: + put: + is: [ secured ] + description: Update the list of wards. + body: + application/json: + schema: wardArray + responses: + 200: + headers: + Keep-Alive: + body: + application/json: + schema: wardArray + 400: + body: + application/json: + schema: localizedErrorResponse + 401: + headers: + Keep-Alive: + body: + application/json: + 403: + body: + application/json: + schema: localizedErrorResponse + 404: + body: + application/json: + schema: localizedErrorResponse /{id}: uriParameters: id: