Compare commits

..

3 Commits

Author SHA1 Message Date
Dev und Test fuer hsadminng
bfbbaed5c3 WIP: addSepaMandateWithBankAccountData 2025-02-24 21:27:29 +01:00
Michael Hoennig
73509990e1 business-glossary entries for Coop-Asset-Transactions-Types and Coop-Share-Transactions-Types 2025-02-24 19:35:54 +01:00
Michael Hoennig
fb216c4769 multiple debitors and subsequent memberships 2025-02-24 19:34:30 +01:00
4 changed files with 102 additions and 61 deletions

View File

@ -2,13 +2,12 @@ package net.hostsharing.hsadminng.hs.office.sepamandate;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.errors.Validate;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeSepaMandatesApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeSepaMandatesApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandatePatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateResource;
import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -20,6 +19,7 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.validation.ValidationException; import jakarta.validation.ValidationException;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException;
import java.util.UUID; import java.util.UUID;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@ -75,6 +75,17 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
final var entityToSave = mapper.map(body, HsOfficeSepaMandateEntity.class, SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER); final var entityToSave = mapper.map(body, HsOfficeSepaMandateEntity.class, SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER);
Validate.validate("bankAccount, bankAccount.uuid").exactlyOne(body.getBankAccount(), body.getBankAccountUuid());
if ( body.getBankAccountUuid() != null) {
entityToSave.setBankAccount(bankAccountRepo.findByUuid(body.getBankAccountUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find BankAccount by bankAccountUuid: " + body.getBankAccountUuid())
));
} else {
entityToSave.setBankAccount(bankAccountRepo.save(
mapper.map(body.getBankAccount(), HsOfficeBankAccountEntity.class)
) );
}
final var saved = sepaMandateRepo.save(entityToSave); final var saved = sepaMandateRepo.save(entityToSave);
final var uri = final var uri =

View File

@ -38,8 +38,6 @@ create table if not exists hs_office.membership
CREATE OR REPLACE FUNCTION hs_office.validate_membership_validity() CREATE OR REPLACE FUNCTION hs_office.validate_membership_validity()
RETURNS trigger AS $$ RETURNS trigger AS $$
DECLARE
partnerNumber int;
BEGIN BEGIN
IF EXISTS ( IF EXISTS (
SELECT 1 SELECT 1
@ -48,10 +46,7 @@ BEGIN
AND uuid <> NEW.uuid AND uuid <> NEW.uuid
AND NEW.validity && validity AND NEW.validity && validity
) THEN ) THEN
SELECT p.partnerNumber INTO partnerNumber RAISE EXCEPTION 'Membership validity ranges overlap for partnerUuid %', NEW.partnerUuid;
FROM hs_office.partner AS p
WHERE p.uuid = NEW.partnerUuid;
RAISE EXCEPTION 'Membership validity ranges overlap for partnerUuid %, partnerNumber %', NEW.partnerUuid, partnerNumber;
END IF; END IF;
RETURN NEW; RETURN NEW;

View File

@ -4,10 +4,10 @@ import io.hypersistence.utils.hibernate.type.range.Range;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealRepository; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealRepository;
import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.grant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.grant.RawRbacGrantRepository;
import net.hostsharing.hsadminng.rbac.role.RawRbacRoleRepository; import net.hostsharing.hsadminng.rbac.role.RawRbacRoleRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.mapper.Array;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tag;
@ -15,9 +15,9 @@ import org.junit.jupiter.api.Test;
import org.postgresql.util.PSQLException; import org.postgresql.util.PSQLException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.orm.jpa.JpaSystemException; import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
@ -32,7 +32,7 @@ import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest @DataJpaTest
@Import({ Context.class, JpaAttempt.class }) @Import( { Context.class, JpaAttempt.class })
@Tag("officeIntegrationTest") @Tag("officeIntegrationTest")
class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCleanup { class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
@ -71,16 +71,15 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0);
// when // when
final var result = attempt( final var result = attempt(em, () -> {
em, () -> { final var newMembership = HsOfficeMembershipEntity.builder()
final var newMembership = HsOfficeMembershipEntity.builder() .memberNumberSuffix("11")
.memberNumberSuffix("11") .partner(givenPartner)
.partner(givenPartner) .validity(Range.closedInfinite(LocalDate.parse("2025-01-01")))
.validity(Range.closedInfinite(LocalDate.parse("2025-01-01"))) .membershipFeeBillable(true)
.membershipFeeBillable(true) .build();
.build(); return toCleanup(membershipRepo.save(newMembership).load());
return toCleanup(membershipRepo.save(newMembership).load()); });
});
// then // then
result.assertSuccessful(); result.assertSuccessful();
@ -93,25 +92,22 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
public void creatingMembershipForSamePartnerIsDisallowedIfAnotherOneIsStillActive() { public void creatingMembershipForSamePartnerIsDisallowedIfAnotherOneIsStillActive() {
// given // given
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
final var count = membershipRepo.count();
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").getFirst(); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").getFirst();
// when // when
final var result = attempt( final var result = attempt(em, () -> {
em, () -> { final var newMembership = HsOfficeMembershipEntity.builder()
final var newMembership = HsOfficeMembershipEntity.builder() .memberNumberSuffix("11")
.memberNumberSuffix("11") .partner(givenPartner)
.partner(givenPartner) .validity(Range.closedInfinite(LocalDate.parse("2024-01-01")))
.validity(Range.closedInfinite(LocalDate.parse("2024-01-01"))) .membershipFeeBillable(true)
.membershipFeeBillable(true) .build();
.build(); return toCleanup(membershipRepo.save(newMembership).load());
return toCleanup(membershipRepo.save(newMembership).load()); });
});
// then // then
result.assertExceptionWithRootCauseMessage( result.assertExceptionWithRootCauseMessage(PSQLException.class, "Membership validity ranges overlap for partnerUuid " + givenPartner.getUuid());
PSQLException.class,
"Membership validity ranges overlap for partnerUuid " + givenPartner.getUuid() +
", partnerNumber " + givenPartner.getPartnerNumber());
} }
@Test @Test
@ -124,17 +120,16 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
.toList(); .toList();
// when // when
attempt( attempt(em, () -> {
em, () -> { final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0);
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0); final var newMembership = HsOfficeMembershipEntity.builder()
final var newMembership = HsOfficeMembershipEntity.builder() .memberNumberSuffix("17")
.memberNumberSuffix("17") .partner(givenPartner)
.partner(givenPartner) .validity(Range.closedInfinite(LocalDate.parse("2025-01-01")))
.validity(Range.closedInfinite(LocalDate.parse("2025-01-01"))) .membershipFeeBillable(true)
.membershipFeeBillable(true) .build();
.build(); return toCleanup(membershipRepo.save(newMembership));
return toCleanup(membershipRepo.save(newMembership)); }).assertSuccessful();
}).assertSuccessful();
// then // then
final var all = rawRoleRepo.findAll(); final var all = rawRoleRepo.findAll();
@ -173,7 +168,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
private void assertThatMembershipIsPersisted(final HsOfficeMembershipEntity saved) { private void assertThatMembershipIsPersisted(final HsOfficeMembershipEntity saved) {
final var found = membershipRepo.findByUuid(saved.getUuid()); final var found = membershipRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString()); assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString()) ;
} }
} }
@ -206,8 +201,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
final var result = membershipRepo.findMembershipsByPartnerUuid(givenPartner.getUuid()); final var result = membershipRepo.findMembershipsByPartnerUuid(givenPartner.getUuid());
// then // then
exactlyTheseMembershipsAreReturned( exactlyTheseMembershipsAreReturned(result,
result,
"Membership(M-1000101, P-10001, [2022-10-01,2024-12-31), ACTIVE)"); "Membership(M-1000101, P-10001, [2022-10-01,2024-12-31), ACTIVE)");
} }
@ -250,8 +244,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
final var result = membershipRepo.findMembershipsByPartnerNumber(10002); final var result = membershipRepo.findMembershipsByPartnerNumber(10002);
// then // then
exactlyTheseMembershipsAreReturned( exactlyTheseMembershipsAreReturned(result,
result,
"Membership(M-1000202, P-10002, [2022-10-01,2026-01-01), ACTIVE)"); "Membership(M-1000202, P-10002, [2022-10-01,2026-01-01), ACTIVE)");
} }
} }
@ -263,7 +256,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
public void globalAdmin_canUpdateValidityOfArbitraryMembership() { public void globalAdmin_canUpdateValidityOfArbitraryMembership() {
// given // given
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
final var givenMembership = givenSomeTemporaryMembership("First", "11"); final var givenMembership = givenSomeTemporaryMembership("First", "11");
assertThatMembershipExistsAndIsAccessibleToCurrentContext(givenMembership); assertThatMembershipExistsAndIsAccessibleToCurrentContext(givenMembership);
final var newValidityEnd = LocalDate.now(); final var newValidityEnd = LocalDate.now();
@ -303,8 +296,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
}); });
// then // then
result.assertExceptionWithRootCauseMessage( result.assertExceptionWithRootCauseMessage(JpaSystemException.class,
JpaSystemException.class,
"[403] Subject ", " is not allowed to update hs_office.membership uuid"); "[403] Subject ", " is not allowed to update hs_office.membership uuid");
} }
@ -412,9 +404,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
"[creating Membership test-data, hs_office.membership, INSERT, 03]"); "[creating Membership test-data, hs_office.membership, INSERT, 03]");
} }
private HsOfficeMembershipEntity givenSomeTemporaryMembership( private HsOfficeMembershipEntity givenSomeTemporaryMembership(final String partnerTradeName, final String memberNumberSuffix) {
final String partnerTradeName,
final String memberNumberSuffix) {
return jpaAttempt.transacted(() -> { return jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike(partnerTradeName).get(0); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike(partnerTradeName).get(0);

View File

@ -137,7 +137,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl
class PostNewSepaMandate { class PostNewSepaMandate {
@Test @Test
void globalAdmin_canPostNewSepaMandate() { void globalAdmin_canPostNewSepaMandateWithBankAccountUuid() {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenDebitor = debitorRepo.findDebitorsByOptionalNameLike("Third").get(0); final var givenDebitor = debitorRepo.findDebitorsByOptionalNameLike("Third").get(0);
@ -177,6 +177,51 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl
assertThat(newSubjectUuid).isNotNull(); assertThat(newSubjectUuid).isNotNull();
} }
@Test
void globalAdmin_canPostNewSepaMandateWithBankAccountData() {
context.define("superuser-alex@hostsharing.net");
final var givenDebitor = debitorRepo.findDebitorsByOptionalNameLike("Third").get(0);
final var location = RestAssured // @formatter:off
.given()
.header("current-subject", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"debitor.uuid": "%s",
"bankAccount": {
"holder": "Fourth eG B",
"iban": "DE02200505501015871393"
"bic": "HASPDEHH"
},
"reference": "temp ref CAT A",
"agreement": "2020-01-02",
"validFrom": "2022-10-13"
}
""".formatted(givenDebitor.getUuid(), givenBankAccount.getUuid()))
.port(port)
.when()
.post("http://localhost/api/hs/office/sepamandates")
.then().log().all().assertThat()
.statusCode(201)
.contentType(ContentType.JSON)
.body("uuid", isUuidValid())
.body("debitor.partner.partnerNumber", is("P-10003"))
.body("bankAccount.holder", is("Fourth eG B"))
.body("bankAccount.iban", is("DE02200505501015871393"))
.body("reference", is("temp ref CAT A"))
.body("validFrom", is("2022-10-13"))
.body("validTo", equalTo(null))
.header("Location", startsWith("http://localhost"))
.extract().header("Location"); // @formatter:on
// finally, the new sepaMandate can be accessed under the generated UUID
final var newSubjectUuid = UUID.fromString(
location.substring(location.lastIndexOf('/') + 1));
assertThat(newSubjectUuid).isNotNull();
}
// TODO.test: move validation tests to a ...WebMvcTest // TODO.test: move validation tests to a ...WebMvcTest
@Test @Test
void globalAdmin_canNotPostNewSepaMandateWhenDebitorUuidIsMissing() { void globalAdmin_canNotPostNewSepaMandateWhenDebitorUuidIsMissing() {