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 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 { + + Optional 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 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 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 actualResult, + final String... coopSharesTransactionNames) { + assertThat(actualResult) + .extracting(coopSharesTransactionEntity -> coopSharesTransactionEntity.toString()) + .containsExactlyInAnyOrder(coopSharesTransactionNames); + } + + void allTheseCoopSharesTransactionsAreReturned( + final List actualResult, + final String... coopSharesTransactionNames) { + assertThat(actualResult) + .extracting(coopSharesTransactionEntity -> coopSharesTransactionEntity.toString()) + .contains(coopSharesTransactionNames); + } +}