Michael Hoennig
2022-10-19 5764accbc5308525a2af740038eed18fcbfed5bf
add hs-office-coopshares API+controller
4 files added
3 files modified
449 ■■■■■ changed files
src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java 96 ●●●●● patch | view | raw | blame | history
src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepository.java 4 ●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-office/hs-office-coopshares-schemas.yaml 54 ●●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-office/hs-office-coopshares.yaml 78 ●●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-office/hs-office.yaml 6 ●●●●● patch | view | raw | blame | history
src/main/resources/db/changelog/318-hs-office-coopshares-test-data.sql 2 ●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java 209 ●●●●● patch | view | raw | blame | history
src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java
New file
@@ -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<List<HsOfficeCoopSharesTransactionResource>> 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<HsOfficeCoopSharesTransactionResource> 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<HsOfficeCoopSharesTransactionEntity, HsOfficeCoopSharesTransactionResource> 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<HsOfficeCoopSharesTransactionInsertResource, HsOfficeCoopSharesTransactionEntity> COOP_SHARES_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
        //        entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo()));
    };
}
src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepository.java
@@ -15,8 +15,8 @@
    @Query("""
            SELECT st FROM HsOfficeCoopSharesTransactionEntity st
                WHERE (:memberNumber IS NULL OR st.membership.memberNumber = :memberNumber)
                    AND (:fromValueDate IS NULL OR (st.valueDate >= :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<HsOfficeCoopSharesTransactionEntity> findCoopSharesTransactionByOptionalMembershipUuidAndDateRange(
src/main/resources/api-definition/hs-office/hs-office-coopshares-schemas.yaml
New file
@@ -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
src/main/resources/api-definition/hs-office/hs-office-coopshares.yaml
New file
@@ -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'
src/main/resources/api-definition/hs-office/hs-office.yaml
@@ -78,3 +78,9 @@
  /api/hs/office/memberships/{membershipUUID}:
    $ref: "./hs-office-memberships-with-uuid.yaml"
  # Coop Shares Transaction
  /api/hs/office/coopsharestransactions:
    $ref: "./hs-office-coopshares.yaml"
src/main/resources/db/changelog/318-hs-office-coopshares-test-data.sql
@@ -25,7 +25,7 @@
        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; $$;
--//
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java
New file
@@ -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();
    }
}