From 5764accbc5308525a2af740038eed18fcbfed5bf Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 19 Oct 2022 10:27:10 +0200 Subject: [PATCH] add hs-office-coopshares API+controller --- ...OfficeCoopSharesTransactionController.java | 96 ++++++++ ...OfficeCoopSharesTransactionRepository.java | 4 +- .../hs-office-coopshares-schemas.yaml | 54 +++++ .../hs-office/hs-office-coopshares.yaml | 78 +++++++ .../api-definition/hs-office/hs-office.yaml | 6 + .../318-hs-office-coopshares-test-data.sql | 2 +- ...esTransactionControllerAcceptanceTest.java | 209 ++++++++++++++++++ 7 files changed, 446 insertions(+), 3 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java create mode 100644 src/main/resources/api-definition/hs-office/hs-office-coopshares-schemas.yaml create mode 100644 src/main/resources/api-definition/hs-office/hs-office-coopshares.yaml create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java new file mode 100644 index 00000000..b37846a0 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java @@ -0,0 +1,96 @@ +package net.hostsharing.hsadminng.hs.office.coopshares; + +import net.hostsharing.hsadminng.Mapper; +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; + +import javax.persistence.EntityManager; +import javax.validation.Valid; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; +import java.util.function.BiConsumer; + +import static net.hostsharing.hsadminng.Mapper.map; + +@RestController + +public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi { + + @Autowired + private Context context; + + @Autowired + private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo; + + @Autowired + private EntityManager em; + + @Override + @Transactional(readOnly = true) + public ResponseEntity> listCoopShares( + final String currentUser, + final String assumedRoles, + final UUID membershipUuid, + Integer memberNumber, // TODO: remove + final @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromValueDate, + final @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toValueDate) { + context.define(currentUser, assumedRoles); + + + final var entities = coopSharesTransactionRepo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange( + memberNumber, + fromValueDate, + toValueDate); + + final var resources = Mapper.mapList(entities, HsOfficeCoopSharesTransactionResource.class, + COOP_SHARES_ENTITY_TO_RESOURCE_POSTMAPPER); + return ResponseEntity.ok(resources); + } + + @Override + @Transactional + public ResponseEntity addCoopSharesTransaction( + final String currentUser, + final String assumedRoles, + @Valid final HsOfficeCoopSharesTransactionInsertResource body) { + + context.define(currentUser, assumedRoles); + + final var entityToSave = map( + body, + HsOfficeCoopSharesTransactionEntity.class, + COOP_SHARES_RESOURCE_TO_ENTITY_POSTMAPPER); + entityToSave.setUuid(UUID.randomUUID()); + + final var saved = coopSharesTransactionRepo.save(entityToSave); + + final var uri = + MvcUriComponentsBuilder.fromController(getClass()) + .path("/api/hs/office/CoopSharesTransactions/{id}") + .buildAndExpand(entityToSave.getUuid()) + .toUri(); + final var mapped = map(saved, HsOfficeCoopSharesTransactionResource.class, + COOP_SHARES_ENTITY_TO_RESOURCE_POSTMAPPER); + return ResponseEntity.created(uri).body(mapped); + } + + final BiConsumer COOP_SHARES_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { + // resource.setValidFrom(entity.getValidity().lower()); + // if (entity.getValidity().hasUpperBound()) { + // resource.setValidTo(entity.getValidity().upper().minusDays(1)); + // } + }; + + final BiConsumer COOP_SHARES_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { + // entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo())); + }; +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepository.java index 4b87b2ee..77360590 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepository.java @@ -15,8 +15,8 @@ public interface HsOfficeCoopSharesTransactionRepository extends Repository= :fromValueDate)) - AND (:toValueDate IS NULL OR (st.valueDate <= :toValueDate)) + AND ( CAST(:fromValueDate AS java.time.LocalDate) IS NULL OR (st.valueDate >= :fromValueDate)) + AND ( CAST(:toValueDate AS java.time.LocalDate)IS NULL OR (st.valueDate <= :toValueDate)) ORDER BY st.membership.memberNumber, st.valueDate """) List findCoopSharesTransactionByOptionalMembershipUuidAndDateRange( diff --git a/src/main/resources/api-definition/hs-office/hs-office-coopshares-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-coopshares-schemas.yaml new file mode 100644 index 00000000..b71fe4bd --- /dev/null +++ b/src/main/resources/api-definition/hs-office/hs-office-coopshares-schemas.yaml @@ -0,0 +1,54 @@ + +components: + + schemas: + + HsOfficeCoopSharesTransactionType: + type: string + enum: + - ADJUSTMENT + - SUBSCRIPTION + - CANCELLATION; + + HsOfficeCoopSharesTransaction: + type: object + properties: + uuid: + type: string + format: uuid + transactionType: + $ref: '#/components/schemas/HsOfficeCoopSharesTransactionType' + sharesCount: + type: integer + valueDate: + type: string + format: date + reference: + type: string + comment: + type: string + + HsOfficeCoopSharesTransactionInsert: + type: object + properties: + membershipUuid: + type: string + format: uuid + nullable: false + transactionType: + $ref: '#/components/schemas/HsOfficeCoopSharesTransactionType' + sharesCount: + type: integer + valueDate: + type: string + format: date + reference: + type: string + comment: + type: string + required: + - transactionType + - sharesCount + - valueDate + - reference + additionalProperties: false diff --git a/src/main/resources/api-definition/hs-office/hs-office-coopshares.yaml b/src/main/resources/api-definition/hs-office/hs-office-coopshares.yaml new file mode 100644 index 00000000..d4ad825a --- /dev/null +++ b/src/main/resources/api-definition/hs-office/hs-office-coopshares.yaml @@ -0,0 +1,78 @@ +get: + summary: Returns a list of (optionally filtered) cooperative share transactions. + description: Returns the list of (optionally filtered) cooperative share transactions which are visible to the current user or any of it's assumed roles. + tags: + - hs-office-coopShares + operationId: listCoopShares + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: membershipUuid + in: query + required: false + schema: + type: string + format: uuid + description: Optional UUID of the related membership. + - name: memberNumber + in: query + required: false + schema: + type: integer + description: Optional member number of the related membership. + - name: fromValueDate + in: query + required: false + schema: + type: string + format: date + description: Optional value date range start (inclusive). + - name: toValueDate + in: query + required: false + schema: + type: string + format: date + description: Optional value date range end (inclusive). + responses: + "200": + description: OK + content: + 'application/json': + schema: + type: array + items: + $ref: './hs-office-coopshares-schemas.yaml#/components/schemas/HsOfficeCoopSharesTransaction' + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + +post: + summary: Adds a new cooperative share transaction. + tags: + - hs-office-coopShares + operationId: addCoopSharesTransaction + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + requestBody: + description: A JSON object describing the new cooperative shares transaction. + required: true + content: + application/json: + schema: + $ref: '/hs-office-coopshares-schemas.yaml#/components/schemas/HsOfficeCoopSharesTransactionInsert' + responses: + "201": + description: Created + content: + 'application/json': + schema: + $ref: './hs-office-coopshares-schemas.yaml#/components/schemas/HsOfficeCoopSharesTransaction' + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + "409": + $ref: './error-responses.yaml#/components/responses/Conflict' diff --git a/src/main/resources/api-definition/hs-office/hs-office.yaml b/src/main/resources/api-definition/hs-office/hs-office.yaml index f6156c86..efcf61cd 100644 --- a/src/main/resources/api-definition/hs-office/hs-office.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office.yaml @@ -78,3 +78,9 @@ paths: /api/hs/office/memberships/{membershipUUID}: $ref: "./hs-office-memberships-with-uuid.yaml" + + + # Coop Shares Transaction + + /api/hs/office/coopsharestransactions: + $ref: "./hs-office-coopshares.yaml" diff --git a/src/main/resources/db/changelog/318-hs-office-coopshares-test-data.sql b/src/main/resources/db/changelog/318-hs-office-coopshares-test-data.sql index 3948272d..9323b09e 100644 --- a/src/main/resources/db/changelog/318-hs-office-coopshares-test-data.sql +++ b/src/main/resources/db/changelog/318-hs-office-coopshares-test-data.sql @@ -25,7 +25,7 @@ begin into hs_office_coopsharestransaction(uuid, membershipuuid, transactiontype, valuedate, sharecount, reference, comment) values (uuid_generate_v4(), membership.uuid, 'SUBSCRIPTION', '2010-03-15', 2, 'ref '||givenMembershipNumber||'-1', 'initial subscription'), - (uuid_generate_v4(), membership.uuid, 'SUBSCRIPTION', '2021-09-01', 24, 'ref '||givenMembershipNumber||'-2', 'subsscibing more'), + (uuid_generate_v4(), membership.uuid, 'SUBSCRIPTION', '2021-09-01', 24, 'ref '||givenMembershipNumber||'-2', 'subscibing more'), (uuid_generate_v4(), membership.uuid, 'CANCELLATION', '2022-10-20', 12, 'ref '||givenMembershipNumber||'-3', 'cancelling some'); end; $$; --// diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java new file mode 100644 index 00000000..76a54570 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java @@ -0,0 +1,209 @@ +package net.hostsharing.hsadminng.hs.office.coopshares; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import net.hostsharing.hsadminng.Accepts; +import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository; +import net.hostsharing.test.JpaAttempt; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import java.util.UUID; + +import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid; +import static net.hostsharing.test.JsonMatcher.lenientlyEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.startsWith; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { HsadminNgApplication.class, JpaAttempt.class } +) +@Transactional +class HsOfficeCoopSharesTransactionControllerAcceptanceTest { + + @LocalServerPort + private Integer port; + + @Autowired + Context context; + + @Autowired + Context contextMock; + + @Autowired + HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo; + + @Autowired + HsOfficeMembershipRepository membershipRepo; + + @Autowired + JpaAttempt jpaAttempt; + + @Autowired + EntityManager em; + + @Nested + @Accepts({ "CoopSharesTransaction:F(Find)" }) + class ListCoopSharesTransactions { + + @Test + void globalAdmin_canViewAllCoopSharesTransactions() { + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/office/coopsharestransactions") + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", hasSize(9)); // @formatter:on + } + + @Test + void globalAdmin_canFindCoopSharesTransactionsByMemberNumber() { + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/office/coopsharestransactions?memberNumber=10002") + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + { + "transactionType": "SUBSCRIPTION", + "sharesCount": 2, + "valueDate": "2010-03-15", + "reference": "ref 10002-1", + "comment": "initial subscription" + }, + { + "transactionType": "SUBSCRIPTION", + "sharesCount": 24, + "valueDate": "2021-09-01", + "reference": "ref 10002-2", + "comment": "subscibing more" + }, + { + "transactionType": "CANCELLATION;", + "sharesCount": 12, + "valueDate": "2022-10-20", + "reference": "ref 10002-3", + "comment": "cancelling some" + } + ] + """)); // @formatter:on + } + + @Test + void globalAdmin_canFindCoopSharesTransactionsByMemberNumberAndDateRange() { + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/office/coopsharestransactions?memberNumber=10002&fromValueDate=2020-01-01&toValueDate=2021-12-31") + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + { + "transactionType": "SUBSCRIPTION", + "sharesCount": 24, + "valueDate": "2021-09-01", + "reference": "ref 10002-2", + "comment": "subscibing more" + } + ] + """)); // @formatter:on + } + } + + @Nested + @Accepts({ "CoopSharesTransaction:C(Create)" }) + class AddCoopSharesTransaction { + + @Test + void globalAdmin_canAddCoopSharesTransaction() { + + context.define("superuser-alex@hostsharing.net"); + final var givenMembership = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber(null, 10001) + .get(0); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "membershipUuid": "%s", + "transactionType": "SUBSCRIPTION", + "sharesCount": 8, + "valueDate": "2022-10-13", + "reference": "temp ref A", + "comment": "just some test coop shares transaction" + } + """.formatted(givenMembership.getUuid())) + .port(port) + .when() + .post("http://localhost/api/hs/office/coopsharestransactions") + .then().log().all().assertThat() + .statusCode(201) + .contentType(ContentType.JSON) + .body("uuid", isUuidValid()) + .body("", lenientlyEquals(""" + { + "transactionType": "SUBSCRIPTION", + "sharesCount": 0, + "valueDate": "2022-10-13", + "reference": "temp ref A", + "comment": "just some test coop shares transaction" + } + """)) + .header("Location", startsWith("http://localhost")) + .extract().header("Location"); // @formatter:on + + // finally, the new coopSharesTransaction can be accessed under the generated UUID + final var newUserUuid = UUID.fromString( + location.substring(location.lastIndexOf('/') + 1)); + assertThat(newUserUuid).isNotNull(); + } + + // TODO.test: move validation tests to a ...WebMvcTest + @Test + void globalAdmin_canNotAddCoopSharesTransactionWhenMembershipUuidIsMissing() { + + } + + } + + @BeforeEach + @AfterEach + void cleanup() { + jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net", null); + // HsOfficeCoopSharesTransactionEntity respectively hs_office_coopsharestransaction_rv + // cannot be deleted at all, but the underlying table record can be deleted. + em.createNativeQuery("delete from hs_office_coopsharestransaction where reference like 'temp %'") + .executeUpdate(); + }).assertSuccessful(); + } +}