From c2dd3d8de9e9a6973d46c1ba2d8296c7d0894d0c Mon Sep 17 00:00:00 2001
From: Michael Hoennig <michael@hoennig.de>
Date: Wed, 19 Oct 2022 07:55:51 +0200
Subject: [PATCH] add hs-office-coopshares entity+repository

---
 .../HsOfficeCoopSharesTransactionEntity.java  |  73 ++++++
 ...OfficeCoopSharesTransactionRepository.java |  28 +++
 .../HsOfficeCoopSharesTransactionType.java    |   5 +
 .../hsadminng/arch/ArchitectureTest.java      |   2 +-
 ...OfficeCoopSharesTransactionEntityTest.java |  33 +++
 ...sTransactionRepositoryIntegrationTest.java | 227 ++++++++++++++++++
 6 files changed, 367 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java
 create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepository.java
 create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionType.java
 create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityTest.java
 create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java

diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java
new file mode 100644
index 00000000..6d34fb3c
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java
@@ -0,0 +1,73 @@
+package net.hostsharing.hsadminng.hs.office.coopshares;
+
+import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType;
+import lombok.*;
+import net.hostsharing.hsadminng.Stringify;
+import net.hostsharing.hsadminng.Stringifyable;
+import net.hostsharing.hsadminng.errors.DisplayName;
+import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
+import org.hibernate.annotations.Type;
+import org.hibernate.annotations.TypeDef;
+
+import javax.persistence.*;
+import java.time.LocalDate;
+import java.util.UUID;
+
+import static net.hostsharing.hsadminng.Stringify.stringify;
+
+@Entity
+@Table(name = "hs_office_coopsharestransaction_rv")
+@TypeDef(
+        name = "pgsql_enum",
+        typeClass = PostgreSQLEnumType.class
+)
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@DisplayName("CoopShareTransaction")
+public class HsOfficeCoopSharesTransactionEntity implements Stringifyable {
+
+    private static Stringify<HsOfficeCoopSharesTransactionEntity> stringify = stringify(HsOfficeCoopSharesTransactionEntity.class)
+            .withProp(e -> e.getMembership().getMemberNumber())
+            .withProp(HsOfficeCoopSharesTransactionEntity::getValueDate)
+            .withProp(HsOfficeCoopSharesTransactionEntity::getTransactionType)
+            .withProp(HsOfficeCoopSharesTransactionEntity::getShareCount)
+            .withProp(HsOfficeCoopSharesTransactionEntity::getReference)
+            .withSeparator(", ")
+            .quotedValues(false);
+
+    private @Id UUID uuid;
+
+    @ManyToOne
+    @JoinColumn(name = "membershipuuid")
+    private HsOfficeMembershipEntity membership;
+
+    @Column(name = "transactiontype")
+    @Enumerated(EnumType.STRING)
+    @Type( type = "pgsql_enum" )
+    private HsOfficeCoopSharesTransactionType transactionType;
+
+    @Column(name = "valuedate")
+    private LocalDate valueDate;
+
+    @Column(name = "sharecount")
+    private int shareCount;
+
+    @Column(name = "reference")
+    private String reference;
+
+    @Column(name = "comment")
+    private String comment;
+
+    @Override
+    public String toString() {
+        return stringify.apply(this);
+    }
+
+    @Override
+    public String toShortString() {
+        return "%s%+d".formatted(membership.getMemberNumber(), shareCount);
+    }
+}
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
new file mode 100644
index 00000000..4b87b2ee
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepository.java
@@ -0,0 +1,28 @@
+package net.hostsharing.hsadminng.hs.office.coopshares;
+
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.Repository;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public interface HsOfficeCoopSharesTransactionRepository extends Repository<HsOfficeCoopSharesTransactionEntity, UUID> {
+
+    Optional<HsOfficeCoopSharesTransactionEntity> findByUuid(UUID id);
+
+    @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))
+                ORDER BY st.membership.memberNumber, st.valueDate
+               """)
+    List<HsOfficeCoopSharesTransactionEntity> findCoopSharesTransactionByOptionalMembershipUuidAndDateRange(
+            Integer memberNumber, LocalDate fromValueDate, LocalDate toValueDate);
+
+    HsOfficeCoopSharesTransactionEntity save(final HsOfficeCoopSharesTransactionEntity entity);
+
+    long count();
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionType.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionType.java
new file mode 100644
index 00000000..fedccc5c
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionType.java
@@ -0,0 +1,5 @@
+package net.hostsharing.hsadminng.hs.office.coopshares;
+
+public enum HsOfficeCoopSharesTransactionType {
+    ADJUSTMENT, SUBSCRIPTION, CANCELLATION;
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java
index 15716bc3..43a4fafb 100644
--- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java
@@ -91,7 +91,7 @@ public class ArchitectureTest {
     public static final ArchRule HsOfficeMembershipPackageRule = classes()
             .that().resideInAPackage("..hs.office.membership..")
             .should().onlyBeAccessed().byClassesThat()
-            .resideInAnyPackage("..hs.office.membership..");
+            .resideInAnyPackage("..hs.office.membership..", "..hs.office.coopshares..");
 
     @ArchTest
     @SuppressWarnings("unused")
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityTest.java
new file mode 100644
index 00000000..3bb43f56
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityTest.java
@@ -0,0 +1,33 @@
+package net.hostsharing.hsadminng.hs.office.coopshares;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+
+import static net.hostsharing.hsadminng.hs.office.membership.TestHsMembership.testMembership;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class HsOfficeCoopSharesTransactionEntityTest {
+
+    final HsOfficeCoopSharesTransactionEntity givenSepaMandate = HsOfficeCoopSharesTransactionEntity.builder()
+            .membership(testMembership)
+            .reference("some-ref")
+            .valueDate(LocalDate.parse("2020-01-01"))
+            .transactionType(HsOfficeCoopSharesTransactionType.SUBSCRIPTION)
+            .shareCount(4)
+            .build();
+
+    @Test
+    void toStringContainsAlmostAllPropertiesAccount() {
+        final var result = givenSepaMandate.toString();
+
+        assertThat(result).isEqualTo("CoopShareTransaction(300001, 2020-01-01, SUBSCRIPTION, 4, some-ref)");
+    }
+
+    @Test
+    void toShortStringContainsOnlyMemberNumberAndSharesCountOnly() {
+        final var result = givenSepaMandate.toShortString();
+
+        assertThat(result).isEqualTo("300001+4");
+    }
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java
new file mode 100644
index 00000000..d5fcac26
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java
@@ -0,0 +1,227 @@
+package net.hostsharing.hsadminng.hs.office.coopshares;
+
+import net.hostsharing.hsadminng.context.Context;
+import net.hostsharing.hsadminng.context.ContextBasedTest;
+import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository;
+import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
+import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
+import net.hostsharing.test.Array;
+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.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.test.annotation.DirtiesContext;
+
+import javax.persistence.EntityManager;
+import javax.servlet.http.HttpServletRequest;
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.grantDisplaysOf;
+import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.roleNamesOf;
+import static net.hostsharing.test.JpaAttempt.attempt;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DataJpaTest
+@ComponentScan(basePackageClasses = { HsOfficeCoopSharesTransactionRepository.class, Context.class, JpaAttempt.class })
+@DirtiesContext
+class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBasedTest {
+
+    @Autowired
+    HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo;
+
+    @Autowired
+    HsOfficeMembershipRepository membershipRepo;
+
+    @Autowired
+    RawRbacRoleRepository rawRoleRepo;
+
+    @Autowired
+    RawRbacGrantRepository rawGrantRepo;
+
+    @Autowired
+    EntityManager em;
+
+    @Autowired
+    JpaAttempt jpaAttempt;
+
+    @MockBean
+    HttpServletRequest request;
+
+    @Nested
+    class CreateCoopSharesTransaction {
+
+        @Test
+        public void globalAdmin_canCreateNewCoopShareTransaction() {
+            // given
+            context("superuser-alex@hostsharing.net");
+            final var count = coopSharesTransactionRepo.count();
+            final var givenMembership = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber(null, 10001)
+                    .get(0);
+
+            // when
+            final var result = attempt(em, () -> {
+                final var newCoopSharesTransaction = HsOfficeCoopSharesTransactionEntity.builder()
+                        .uuid(UUID.randomUUID())
+                        .membership(givenMembership)
+                        .transactionType(HsOfficeCoopSharesTransactionType.SUBSCRIPTION)
+                        .shareCount(4)
+                        .valueDate(LocalDate.parse("2022-10-18"))
+                        .reference("temp ref A")
+                        .build();
+                return coopSharesTransactionRepo.save(newCoopSharesTransaction);
+            });
+
+            // then
+            result.assertSuccessful();
+            assertThat(result.returnedValue()).isNotNull().extracting(HsOfficeCoopSharesTransactionEntity::getUuid).isNotNull();
+            assertThatCoopSharesTransactionIsPersisted(result.returnedValue());
+            assertThat(coopSharesTransactionRepo.count()).isEqualTo(count + 1);
+        }
+
+        @Test
+        public void createsAndGrantsRoles() {
+            // given
+            context("superuser-alex@hostsharing.net");
+            final var initialRoleNames = roleNamesOf(rawRoleRepo.findAll());
+            final var initialGrantNames = grantDisplaysOf(rawGrantRepo.findAll()).stream()
+                    .map(s -> s.replace("FirstGmbH-firstcontact", "..."))
+                    .map(s -> s.replace("hs_office_", ""))
+                    .toList();
+
+            // when
+            attempt(em, () -> {
+                final var givenMembership = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber(
+                        null,
+                        10001).get(0);
+                final var newCoopSharesTransaction = HsOfficeCoopSharesTransactionEntity.builder()
+                        .uuid(UUID.randomUUID())
+                        .membership(givenMembership)
+                        .transactionType(HsOfficeCoopSharesTransactionType.SUBSCRIPTION)
+                        .shareCount(4)
+                        .valueDate(LocalDate.parse("2022-10-18"))
+                        .reference("temp ref B")
+                        .build();
+                return coopSharesTransactionRepo.save(newCoopSharesTransaction);
+            });
+
+            // then
+            final var all = rawRoleRepo.findAll();
+            assertThat(roleNamesOf(all)).containsExactlyInAnyOrder(Array.from(initialRoleNames)); // no new roles created
+            assertThat(grantDisplaysOf(rawGrantRepo.findAll()))
+                    .map(s -> s.replace("FirstGmbH-firstcontact", "..."))
+                    .map(s -> s.replace("hs_office_", ""))
+                    .containsExactlyInAnyOrder(Array.fromFormatted(
+                            initialGrantNames,
+                            "{ grant perm view on coopsharestransaction#temprefB to role membership#10001....tenant by system and assume }",
+                            null));
+        }
+
+        private void assertThatCoopSharesTransactionIsPersisted(final HsOfficeCoopSharesTransactionEntity saved) {
+            final var found = coopSharesTransactionRepo.findByUuid(saved.getUuid());
+            assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved);
+        }
+    }
+
+    @Nested
+    class FindAllCoopSharesTransactions {
+
+        @Test
+        public void globalAdmin_withoutAssumedRole_canViewAllCoopSharesTransactions() {
+            // given
+            context("superuser-alex@hostsharing.net");
+
+            // when
+            final var result = coopSharesTransactionRepo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange(
+                    null,
+                    null,
+                    null);
+
+            // then
+            allTheseCoopSharesTransactionsAreReturned(
+                    result,
+                    "CoopShareTransaction(10001, 2010-03-15, SUBSCRIPTION, 2, ref 10001-1)",
+                    "CoopShareTransaction(10001, 2021-09-01, SUBSCRIPTION, 24, ref 10001-2)",
+                    "CoopShareTransaction(10001, 2022-10-20, CANCELLATION, 12, ref 10001-3)",
+
+                    "CoopShareTransaction(10002, 2010-03-15, SUBSCRIPTION, 2, ref 10002-1)",
+                    "CoopShareTransaction(10002, 2021-09-01, SUBSCRIPTION, 24, ref 10002-2)",
+                    "CoopShareTransaction(10002, 2022-10-20, CANCELLATION, 12, ref 10002-3)",
+
+                    "CoopShareTransaction(10003, 2010-03-15, SUBSCRIPTION, 2, ref 10003-1)",
+                    "CoopShareTransaction(10003, 2021-09-01, SUBSCRIPTION, 24, ref 10003-2)",
+                    "CoopShareTransaction(10003, 2022-10-20, CANCELLATION, 12, ref 10003-3)");
+        }
+
+        @Test
+        public void normalUser_canViewOnlyRelatedCoopSharesTransactions() {
+            // given:
+            context("superuser-alex@hostsharing.net", "hs_office_partner#FirstGmbH-firstcontact.admin");
+            //                    "hs_office_person#FirstGmbH.admin",
+
+            // when:
+            final var result = coopSharesTransactionRepo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange(
+                    null,
+                    null,
+                    null);
+
+            // then:
+            exactlyTheseCoopSharesTransactionsAreReturned(
+                    result,
+                    "CoopShareTransaction(10001, 2010-03-15, SUBSCRIPTION, 2, ref 10001-1)",
+                    "CoopShareTransaction(10001, 2021-09-01, SUBSCRIPTION, 24, ref 10001-2)",
+                    "CoopShareTransaction(10001, 2022-10-20, CANCELLATION, 12, ref 10001-3)");
+        }
+    }
+
+    @Test
+    public void auditJournalLogIsAvailable() {
+        // given
+        final var query = em.createNativeQuery("""
+                select c.currenttask, j.targettable, j.targetop
+                    from tx_journal j
+                    join tx_context c on j.contextId = c.contextId
+                    where targettable = 'hs_office_coopsharestransaction';
+                    """);
+
+        // when
+        @SuppressWarnings("unchecked") final List<Object[]> customerLogEntries = query.getResultList();
+
+        // then
+        assertThat(customerLogEntries).map(Arrays::toString).contains(
+                "[creating coopSharesTransaction test-data 10001, hs_office_coopsharestransaction, INSERT]",
+                "[creating coopSharesTransaction test-data 10002, hs_office_coopsharestransaction, INSERT]");
+    }
+
+    @BeforeEach
+    @AfterEach
+    void cleanup() {
+        jpaAttempt.transacted(() -> {
+            context("superuser-alex@hostsharing.net", null);
+            em.createQuery("DELETE FROM HsOfficeCoopSharesTransactionEntity WHERE reference like 'temp ref%'");
+        });
+    }
+
+    void exactlyTheseCoopSharesTransactionsAreReturned(
+            final List<HsOfficeCoopSharesTransactionEntity> actualResult,
+            final String... coopSharesTransactionNames) {
+        assertThat(actualResult)
+                .extracting(coopSharesTransactionEntity -> coopSharesTransactionEntity.toString())
+                .containsExactlyInAnyOrder(coopSharesTransactionNames);
+    }
+
+    void allTheseCoopSharesTransactionsAreReturned(
+            final List<HsOfficeCoopSharesTransactionEntity> actualResult,
+            final String... coopSharesTransactionNames) {
+        assertThat(actualResult)
+                .extracting(coopSharesTransactionEntity -> coopSharesTransactionEntity.toString())
+                .contains(coopSharesTransactionNames);
+    }
+}