scenario test for multiple debitors+memberships + validation for subsequent memberships #160
@ -108,6 +108,40 @@ der Person des _Subscriber-Contact_ (_Holder_) zur repräsentierten Person (_Anc
|
|||||||
Zusätzlich wird diese Relation mit dem Kurznamen der abonnierten Mailingliste markiert.
|
Zusätzlich wird diese Relation mit dem Kurznamen der abonnierten Mailingliste markiert.
|
||||||
|
|
||||||
|
|
||||||
|
### Coop-Asset-Transactions (Geschäftsguthabens-Transaktionen)
|
||||||
|
|
||||||
|
- positiver Wert => Geschäftsguthaben nehmen zu
|
||||||
|
- negativer Wert => Geschäftsguthaben nehmen ab
|
||||||
|
|
||||||
|
**REVERSAL**: **Korrekturbuchung** einer fehlerhaften Buchung, positiver oder negativer Wert ist möglich
|
||||||
|
|
||||||
|
**DEPOSIT**: **Zahlungseingang** vom Mitglied nach Beteiligung mit Geschäftsanteilen, immer positiver Wert
|
||||||
|
|
||||||
|
**DISBURSAL**: **Zahlungsausgang** an Mitglied nach Kündigung von Geschäftsanteilen, immer negativer Wert
|
||||||
|
|
||||||
|
**TRANSFER**: **Übertragung** von Geschäftsguthaben an ein anderes Mitglied, immer negativer Wert
|
||||||
|
|
||||||
|
**ADOPTION**: **Übernahme** von Geschäftsguthaben von einem anderen Mitglied, immer positiver Wert
|
||||||
|
|
||||||
|
**CLEARING**: **Verrechnung** von Geschäftsguthaben mit Schulden des Mitglieds, immer negativer Wert
|
||||||
|
|
||||||
|
**LOSS**: **Verlust** von Geschäftsguthaben bei Zuweisung Eigenkapitalverlust nach Kündigung von Geschäftsanteilen, immer negativer Wert
|
||||||
|
|
||||||
|
**LIMITATION**: **Verjährung** von Geschäftsguthaben, wenn Auszahlung innerhalb der Frist nicht möglich war.
|
||||||
|
|
||||||
|
|
||||||
|
### Coop-Share-Transactions (Geschäftsanteil-Transaktionen)
|
||||||
|
|
||||||
|
- positiver Wert => Geschäftsanteile nehmen zu
|
||||||
|
- negativer Wert => Geschäftsanteile nehmen ab
|
||||||
|
-
|
||||||
|
**REVERSAL**: **Korrekturbuchung** einer fehlerhaften Buchung, positiver oder negativer Wert ist möglich
|
||||||
|
|
||||||
|
**SUBSCRIPTION**: **Beteiligung** mit Geschäftsanteilen, z.B. durch Beitrittserklärung, immer positiver Wert
|
||||||
|
|
||||||
|
**CANCELLATION**: **Kündigung** von Geschäftsanteilen, z.B. durch Austritt, immer negativer Wert
|
||||||
|
|
||||||
|
|
||||||
#### Anchor / Relation-Anchor
|
#### Anchor / Relation-Anchor
|
||||||
|
|
||||||
siehe [Relation](#Relation)
|
siehe [Relation](#Relation)
|
||||||
|
@ -116,7 +116,7 @@ classDiagram
|
|||||||
+BankAccount refundBankAccount
|
+BankAccount refundBankAccount
|
||||||
+String defaultPrefix: mei
|
+String defaultPrefix: mei
|
||||||
}
|
}
|
||||||
debitor-MeierGmbH o-- partner-MeierGmbH
|
debitor-MeierGmbH o.. partner-MeierGmbH
|
||||||
debitor-MeierGmbH *-- rel-MeierGmbH-Buha
|
debitor-MeierGmbH *-- rel-MeierGmbH-Buha
|
||||||
|
|
||||||
class contactData-MeierGmbH-Buha {
|
class contactData-MeierGmbH-Buha {
|
||||||
|
@ -32,6 +32,41 @@ create table if not exists hs_office.membership
|
|||||||
--//
|
--//
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
--changeset michael.hoennig:hs-office-membership-SINGLE-MEMBERSHIP-CHECK endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION hs_office.validate_membership_validity()
|
||||||
|
RETURNS trigger AS $$
|
||||||
|
DECLARE
|
||||||
|
partnerNumber int;
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM hs_office.membership
|
||||||
|
WHERE partnerUuid = NEW.partnerUuid
|
||||||
|
AND uuid <> NEW.uuid
|
||||||
|
AND NEW.validity && validity
|
||||||
|
) THEN
|
||||||
|
SELECT p.partnerNumber INTO partnerNumber
|
||||||
|
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;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_validate_membership_validity
|
||||||
|
BEFORE INSERT OR UPDATE ON hs_office.membership
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION hs_office.validate_membership_validity();
|
||||||
|
|
||||||
|
|
||||||
|
--//
|
||||||
|
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
--changeset michael.hoennig:hs-office-membership-MAIN-TABLE-JOURNAL endDelimiter:--//
|
--changeset michael.hoennig:hs-office-membership-MAIN-TABLE-JOURNAL endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
|
@ -10,7 +10,8 @@
|
|||||||
*/
|
*/
|
||||||
create or replace procedure hs_office.membership_create_test_data(
|
create or replace procedure hs_office.membership_create_test_data(
|
||||||
forPartnerNumber numeric(5),
|
forPartnerNumber numeric(5),
|
||||||
newMemberNumberSuffix char(2) )
|
newMemberNumberSuffix char(2),
|
||||||
|
validity daterange)
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
declare
|
declare
|
||||||
relatedPartner hs_office.partner;
|
relatedPartner hs_office.partner;
|
||||||
@ -22,7 +23,7 @@ begin
|
|||||||
raise notice '- using partner (%): %', relatedPartner.uuid, relatedPartner;
|
raise notice '- using partner (%): %', relatedPartner.uuid, relatedPartner;
|
||||||
insert
|
insert
|
||||||
into hs_office.membership (uuid, partneruuid, memberNumberSuffix, validity, status)
|
into hs_office.membership (uuid, partneruuid, memberNumberSuffix, validity, status)
|
||||||
values (uuid_generate_v4(), relatedPartner.uuid, newMemberNumberSuffix, daterange('20221001' , null, '[]'), 'ACTIVE');
|
values (uuid_generate_v4(), relatedPartner.uuid, newMemberNumberSuffix, validity, 'ACTIVE');
|
||||||
end; $$;
|
end; $$;
|
||||||
--//
|
--//
|
||||||
|
|
||||||
@ -35,9 +36,9 @@ do language plpgsql $$
|
|||||||
begin
|
begin
|
||||||
call base.defineContext('creating Membership test-data', null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN');
|
call base.defineContext('creating Membership test-data', null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN');
|
||||||
|
|
||||||
call hs_office.membership_create_test_data(10001, '01');
|
call hs_office.membership_create_test_data(10001, '01', daterange('20221001' , '20241231', '[)'));
|
||||||
call hs_office.membership_create_test_data(10002, '02');
|
call hs_office.membership_create_test_data(10002, '02', daterange('20221001' , '20251231', '[]'));
|
||||||
call hs_office.membership_create_test_data(10003, '03');
|
call hs_office.membership_create_test_data(10003, '03', daterange('20221001' , null, '[]'));
|
||||||
end;
|
end;
|
||||||
$$;
|
$$;
|
||||||
--//
|
--//
|
||||||
|
@ -86,7 +86,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
"memberNumber": "M-1000101",
|
"memberNumber": "M-1000101",
|
||||||
"memberNumberSuffix": "01",
|
"memberNumberSuffix": "01",
|
||||||
"validFrom": "2022-10-01",
|
"validFrom": "2022-10-01",
|
||||||
"validTo": null,
|
"validTo": "2024-12-30",
|
||||||
"status": "ACTIVE"
|
"status": "ACTIVE"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -94,7 +94,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
"memberNumber": "M-1000202",
|
"memberNumber": "M-1000202",
|
||||||
"memberNumberSuffix": "02",
|
"memberNumberSuffix": "02",
|
||||||
"validFrom": "2022-10-01",
|
"validFrom": "2022-10-01",
|
||||||
"validTo": null,
|
"validTo": "2025-12-31",
|
||||||
"status": "ACTIVE"
|
"status": "ACTIVE"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -133,7 +133,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
"memberNumber": "M-1000101",
|
"memberNumber": "M-1000101",
|
||||||
"memberNumberSuffix": "01",
|
"memberNumberSuffix": "01",
|
||||||
"validFrom": "2022-10-01",
|
"validFrom": "2022-10-01",
|
||||||
"validTo": null,
|
"validTo": "2024-12-30",
|
||||||
"status": "ACTIVE"
|
"status": "ACTIVE"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -161,7 +161,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
"memberNumber": "M-1000202",
|
"memberNumber": "M-1000202",
|
||||||
"memberNumberSuffix": "02",
|
"memberNumberSuffix": "02",
|
||||||
"validFrom": "2022-10-01",
|
"validFrom": "2022-10-01",
|
||||||
"validTo": null,
|
"validTo": "2025-12-31",
|
||||||
"status": "ACTIVE"
|
"status": "ACTIVE"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -177,7 +177,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
void globalAdmin_canAddMembership() {
|
void globalAdmin_canAddMembership() {
|
||||||
|
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Third").get(0);
|
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").getFirst();
|
||||||
final var givenMemberSuffix = TEMP_MEMBER_NUMBER_SUFFIX;
|
final var givenMemberSuffix = TEMP_MEMBER_NUMBER_SUFFIX;
|
||||||
final var expectedMemberNumber = Integer.parseInt(givenPartner.getPartnerNumber() + TEMP_MEMBER_NUMBER_SUFFIX);
|
final var expectedMemberNumber = Integer.parseInt(givenPartner.getPartnerNumber() + TEMP_MEMBER_NUMBER_SUFFIX);
|
||||||
|
|
||||||
@ -189,7 +189,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
{
|
{
|
||||||
"partner.uuid": "%s",
|
"partner.uuid": "%s",
|
||||||
"memberNumberSuffix": "%s",
|
"memberNumberSuffix": "%s",
|
||||||
"validFrom": "2022-10-13",
|
"validFrom": "2025-02-13",
|
||||||
"membershipFeeBillable": "true"
|
"membershipFeeBillable": "true"
|
||||||
}
|
}
|
||||||
""".formatted(givenPartner.getUuid(), givenMemberSuffix))
|
""".formatted(givenPartner.getUuid(), givenMemberSuffix))
|
||||||
@ -200,10 +200,10 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
.statusCode(201)
|
.statusCode(201)
|
||||||
.contentType(ContentType.JSON)
|
.contentType(ContentType.JSON)
|
||||||
.body("uuid", isUuidValid())
|
.body("uuid", isUuidValid())
|
||||||
.body("partner.partnerNumber", is("P-10003"))
|
.body("partner.partnerNumber", is("P-10001"))
|
||||||
.body("memberNumber", is("M-" + expectedMemberNumber))
|
.body("memberNumber", is("M-" + expectedMemberNumber))
|
||||||
.body("memberNumberSuffix", is(givenMemberSuffix))
|
.body("memberNumberSuffix", is(givenMemberSuffix))
|
||||||
.body("validFrom", is("2022-10-13"))
|
.body("validFrom", is("2025-02-13"))
|
||||||
.body("validTo", equalTo(null))
|
.body("validTo", equalTo(null))
|
||||||
.header("Location", startsWith("http://localhost"))
|
.header("Location", startsWith("http://localhost"))
|
||||||
.extract().header("Location"); // @formatter:on
|
.extract().header("Location"); // @formatter:on
|
||||||
@ -239,7 +239,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
"memberNumber": "M-1000101",
|
"memberNumber": "M-1000101",
|
||||||
"memberNumberSuffix": "01",
|
"memberNumberSuffix": "01",
|
||||||
"validFrom": "2022-10-01",
|
"validFrom": "2022-10-01",
|
||||||
"validTo": null,
|
"validTo": "2024-12-30",
|
||||||
"status": "ACTIVE"
|
"status": "ACTIVE"
|
||||||
}
|
}
|
||||||
""")); // @formatter:on
|
""")); // @formatter:on
|
||||||
@ -297,13 +297,13 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
final var givenMembership = givenSomeTemporaryMembershipBessler("First");
|
final var givenMembership = givenSomeTemporaryMembershipBessler("First");
|
||||||
|
|
||||||
final var location = RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
.header("current-subject", "superuser-alex@hostsharing.net")
|
.header("current-subject", "superuser-alex@hostsharing.net")
|
||||||
.contentType(ContentType.JSON)
|
.contentType(ContentType.JSON)
|
||||||
.body("""
|
.body("""
|
||||||
{
|
{
|
||||||
"validTo": "2023-12-31",
|
"validTo": "2025-12-31",
|
||||||
"status": "CANCELLED"
|
"status": "CANCELLED"
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
@ -316,8 +316,8 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
.body("uuid", isUuidValid())
|
.body("uuid", isUuidValid())
|
||||||
.body("partner.partnerNumber", is("P-" + givenMembership.getPartner().getPartnerNumber()))
|
.body("partner.partnerNumber", is("P-" + givenMembership.getPartner().getPartnerNumber()))
|
||||||
.body("memberNumberSuffix", is(givenMembership.getMemberNumberSuffix()))
|
.body("memberNumberSuffix", is(givenMembership.getMemberNumberSuffix()))
|
||||||
.body("validFrom", is("2022-11-01"))
|
.body("validFrom", is("2025-02-01"))
|
||||||
.body("validTo", is("2023-12-31"))
|
.body("validTo", is("2025-12-31"))
|
||||||
.body("status", is("CANCELLED"));
|
.body("status", is("CANCELLED"));
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
@ -326,7 +326,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
.matches(mandate -> {
|
.matches(mandate -> {
|
||||||
assertThat(mandate.getPartner().toShortString()).isEqualTo("P-10001");
|
assertThat(mandate.getPartner().toShortString()).isEqualTo("P-10001");
|
||||||
assertThat(mandate.getMemberNumberSuffix()).isEqualTo(givenMembership.getMemberNumberSuffix());
|
assertThat(mandate.getMemberNumberSuffix()).isEqualTo(givenMembership.getMemberNumberSuffix());
|
||||||
assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2024-01-01)");
|
assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2026-01-01)");
|
||||||
assertThat(mandate.getStatus()).isEqualTo(CANCELLED);
|
assertThat(mandate.getStatus()).isEqualTo(CANCELLED);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@ -348,7 +348,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
.contentType(ContentType.JSON)
|
.contentType(ContentType.JSON)
|
||||||
.body("""
|
.body("""
|
||||||
{
|
{
|
||||||
"validTo": "2024-01-01",
|
"validTo": "2025-12-31",
|
||||||
"status": "CANCELLED"
|
"status": "CANCELLED"
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
@ -361,7 +361,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
// finally, the Membership is actually updated
|
// finally, the Membership is actually updated
|
||||||
assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isPresent().get()
|
assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isPresent().get()
|
||||||
.matches(mandate -> {
|
.matches(mandate -> {
|
||||||
assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2024-01-02)");
|
assertThat(mandate.getValidity().asString()).isEqualTo("[2025-02-01,2026-01-01)");
|
||||||
assertThat(mandate.getStatus()).isEqualTo(CANCELLED);
|
assertThat(mandate.getStatus()).isEqualTo(CANCELLED);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@ -434,7 +434,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
|
|||||||
final var newMembership = HsOfficeMembershipEntity.builder()
|
final var newMembership = HsOfficeMembershipEntity.builder()
|
||||||
.partner(givenPartner)
|
.partner(givenPartner)
|
||||||
.memberNumberSuffix(TEMP_MEMBER_NUMBER_SUFFIX)
|
.memberNumberSuffix(TEMP_MEMBER_NUMBER_SUFFIX)
|
||||||
.validity(Range.closedInfinite(LocalDate.parse("2022-11-01")))
|
.validity(Range.closedInfinite(LocalDate.parse("2025-02-01")))
|
||||||
.status(ACTIVE)
|
.status(ACTIVE)
|
||||||
.membershipFeeBillable(true)
|
.membershipFeeBillable(true)
|
||||||
.build();
|
.build();
|
||||||
|
@ -4,19 +4,20 @@ 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.rbac.test.ContextBasedTestWithCleanup;
|
import net.hostsharing.hsadminng.mapper.Array;
|
||||||
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.mapper.Array;
|
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||||
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;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
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;
|
||||||
@ -31,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 {
|
||||||
|
|
||||||
@ -70,11 +71,12 @@ 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(em, () -> {
|
final var result = attempt(
|
||||||
|
em, () -> {
|
||||||
final var newMembership = HsOfficeMembershipEntity.builder()
|
final var newMembership = HsOfficeMembershipEntity.builder()
|
||||||
.memberNumberSuffix("11")
|
.memberNumberSuffix("11")
|
||||||
.partner(givenPartner)
|
.partner(givenPartner)
|
||||||
.validity(Range.closedInfinite(LocalDate.parse("2020-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());
|
||||||
@ -87,6 +89,31 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
|
|||||||
assertThat(membershipRepo.count()).isEqualTo(count + 1);
|
assertThat(membershipRepo.count()).isEqualTo(count + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void creatingMembershipForSamePartnerIsDisallowedIfAnotherOneIsStillActive() {
|
||||||
|
// given
|
||||||
|
context("superuser-alex@hostsharing.net");
|
||||||
|
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").getFirst();
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var result = attempt(
|
||||||
|
em, () -> {
|
||||||
|
final var newMembership = HsOfficeMembershipEntity.builder()
|
||||||
|
.memberNumberSuffix("11")
|
||||||
|
.partner(givenPartner)
|
||||||
|
.validity(Range.closedInfinite(LocalDate.parse("2024-01-01")))
|
||||||
|
.membershipFeeBillable(true)
|
||||||
|
.build();
|
||||||
|
return toCleanup(membershipRepo.save(newMembership).load());
|
||||||
|
});
|
||||||
|
|
||||||
|
// then
|
||||||
|
result.assertExceptionWithRootCauseMessage(
|
||||||
|
PSQLException.class,
|
||||||
|
"Membership validity ranges overlap for partnerUuid " + givenPartner.getUuid() +
|
||||||
|
", partnerNumber " + givenPartner.getPartnerNumber());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void createsAndGrantsRoles() {
|
public void createsAndGrantsRoles() {
|
||||||
// given
|
// given
|
||||||
@ -97,12 +124,13 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
attempt(em, () -> {
|
attempt(
|
||||||
|
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("2020-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));
|
||||||
@ -145,7 +173,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,8 +191,8 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
|
|||||||
// then
|
// then
|
||||||
exactlyTheseMembershipsAreReturned(
|
exactlyTheseMembershipsAreReturned(
|
||||||
result,
|
result,
|
||||||
"Membership(M-1000101, P-10001, [2022-10-01,), ACTIVE)",
|
"Membership(M-1000101, P-10001, [2022-10-01,2024-12-31), ACTIVE)",
|
||||||
"Membership(M-1000202, P-10002, [2022-10-01,), ACTIVE)",
|
"Membership(M-1000202, P-10002, [2022-10-01,2026-01-01), ACTIVE)",
|
||||||
"Membership(M-1000303, P-10003, [2022-10-01,), ACTIVE)");
|
"Membership(M-1000303, P-10003, [2022-10-01,), ACTIVE)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,8 +206,9 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
|
|||||||
final var result = membershipRepo.findMembershipsByPartnerUuid(givenPartner.getUuid());
|
final var result = membershipRepo.findMembershipsByPartnerUuid(givenPartner.getUuid());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
exactlyTheseMembershipsAreReturned(result,
|
exactlyTheseMembershipsAreReturned(
|
||||||
"Membership(M-1000101, P-10001, [2022-10-01,), ACTIVE)");
|
result,
|
||||||
|
"Membership(M-1000101, P-10001, [2022-10-01,2024-12-31), ACTIVE)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -194,7 +223,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
|
|||||||
assertThat(result)
|
assertThat(result)
|
||||||
.isNotNull()
|
.isNotNull()
|
||||||
.extracting(Object::toString)
|
.extracting(Object::toString)
|
||||||
.isEqualTo("Membership(M-1000202, P-10002, [2022-10-01,), ACTIVE)");
|
.isEqualTo("Membership(M-1000202, P-10002, [2022-10-01,2026-01-01), ACTIVE)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -209,7 +238,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
|
|||||||
assertThat(result)
|
assertThat(result)
|
||||||
.isNotNull()
|
.isNotNull()
|
||||||
.extracting(Object::toString)
|
.extracting(Object::toString)
|
||||||
.isEqualTo("Membership(M-1000202, P-10002, [2022-10-01,), ACTIVE)");
|
.isEqualTo("Membership(M-1000202, P-10002, [2022-10-01,2026-01-01), ACTIVE)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -221,8 +250,9 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
|
|||||||
final var result = membershipRepo.findMembershipsByPartnerNumber(10002);
|
final var result = membershipRepo.findMembershipsByPartnerNumber(10002);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
exactlyTheseMembershipsAreReturned(result,
|
exactlyTheseMembershipsAreReturned(
|
||||||
"Membership(M-1000202, P-10002, [2022-10-01,), ACTIVE)");
|
result,
|
||||||
|
"Membership(M-1000202, P-10002, [2022-10-01,2026-01-01), ACTIVE)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,7 +303,8 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
|
|||||||
});
|
});
|
||||||
|
|
||||||
// then
|
// then
|
||||||
result.assertExceptionWithRootCauseMessage(JpaSystemException.class,
|
result.assertExceptionWithRootCauseMessage(
|
||||||
|
JpaSystemException.class,
|
||||||
"[403] Subject ", " is not allowed to update hs_office.membership uuid");
|
"[403] Subject ", " is not allowed to update hs_office.membership uuid");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,14 +412,16 @@ 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(final String partnerTradeName, final String memberNumberSuffix) {
|
private HsOfficeMembershipEntity givenSomeTemporaryMembership(
|
||||||
|
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);
|
||||||
final var newMembership = HsOfficeMembershipEntity.builder()
|
final var newMembership = HsOfficeMembershipEntity.builder()
|
||||||
.memberNumberSuffix(memberNumberSuffix)
|
.memberNumberSuffix(memberNumberSuffix)
|
||||||
.partner(givenPartner)
|
.partner(givenPartner)
|
||||||
.validity(Range.closedInfinite(LocalDate.parse("2020-01-01")))
|
.validity(Range.closedInfinite(LocalDate.parse("2025-02-01")))
|
||||||
.membershipFeeBillable(true)
|
.membershipFeeBillable(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -287,12 +287,12 @@ class HsOfficeScenarioTests extends ScenarioTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Order(2011)
|
@Order(2011)
|
||||||
@Requires("Person: Test AG")
|
@Requires("Debitor: D-3101000 - Test AG - main debitor")
|
||||||
@Produces("Debitor: D-3101001 - Test AG - main debitor")
|
@Produces("Debitor: D-3101001 - Test AG - additional debitor")
|
||||||
void shouldCreateExternalDebitorForPartner() {
|
void shouldCreateAdditionDebitorForPartner() {
|
||||||
new CreateExternalDebitorForPartner(scenarioTest)
|
new CreateSelfDebitorForPartner(scenarioTest)
|
||||||
.given("partnerPersonTradeName", "Test AG")
|
.given("partnerPersonTradeName", "Test AG")
|
||||||
.given("billingContactCaption", "Billing GmbH - billing department")
|
.given("billingContactCaption", "Test AG - billing department")
|
||||||
.given("billingContactEmailAddress", "billing@test-ag.example.org")
|
.given("billingContactEmailAddress", "billing@test-ag.example.org")
|
||||||
.given("debitorNumberSuffix", "01")
|
.given("debitorNumberSuffix", "01")
|
||||||
.given("billable", true)
|
.given("billable", true)
|
||||||
@ -305,10 +305,30 @@ class HsOfficeScenarioTests extends ScenarioTest {
|
|||||||
.keep();
|
.keep();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(2012)
|
||||||
|
@Requires("Person: Test AG")
|
||||||
|
@Produces("Debitor: D-3101002 - Test AG - external debitor")
|
||||||
|
void shouldCreateExternalDebitorForPartner() {
|
||||||
|
new CreateExternalDebitorForPartner(scenarioTest)
|
||||||
|
.given("partnerPersonTradeName", "Test AG")
|
||||||
|
.given("billingContactCaption", "Billing GmbH - billing department")
|
||||||
|
.given("billingContactEmailAddress", "billing@test-ag.example.org")
|
||||||
|
.given("debitorNumberSuffix", "02")
|
||||||
|
.given("billable", true)
|
||||||
|
.given("vatId", "VAT123456")
|
||||||
|
.given("vatCountryCode", "DE")
|
||||||
|
.given("vatBusiness", true)
|
||||||
|
.given("vatReverseCharge", false)
|
||||||
|
.given("defaultPrefix", "tsy")
|
||||||
|
.doRun()
|
||||||
|
.keep();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Order(2020)
|
@Order(2020)
|
||||||
@Requires("Person: Test AG")
|
@Requires("Person: Test AG")
|
||||||
@Produces(explicitly = "Debitor: D-3101000 - Test AG - delete debitor", permanent = false)
|
@Produces(explicitly = "Debitor: D-3101002 - Test AG - delete debitor", permanent = false)
|
||||||
void shouldDeleteDebitor() {
|
void shouldDeleteDebitor() {
|
||||||
new DeleteDebitor(scenarioTest)
|
new DeleteDebitor(scenarioTest)
|
||||||
.given("partnerNumber", "P-31020")
|
.given("partnerNumber", "P-31020")
|
||||||
@ -317,7 +337,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Order(2020)
|
@Order(2021)
|
||||||
@Requires("Debitor: D-3101000 - Test AG - main debitor")
|
@Requires("Debitor: D-3101000 - Test AG - main debitor")
|
||||||
@Disabled("see TODO.spec in DontDeleteDefaultDebitor")
|
@Disabled("see TODO.spec in DontDeleteDefaultDebitor")
|
||||||
void shouldNotDeleteDefaultDebitor() {
|
void shouldNotDeleteDefaultDebitor() {
|
||||||
@ -387,7 +407,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
|
|||||||
void shouldCreateMembershipForPartner() {
|
void shouldCreateMembershipForPartner() {
|
||||||
new CreateMembership(scenarioTest)
|
new CreateMembership(scenarioTest)
|
||||||
.given("partnerName", "Test AG")
|
.given("partnerName", "Test AG")
|
||||||
.given("validFrom", "2024-10-15")
|
.given("validFrom", "2020-10-15")
|
||||||
.given("newStatus", "ACTIVE")
|
.given("newStatus", "ACTIVE")
|
||||||
.given("membershipFeeBillable", "true")
|
.given("membershipFeeBillable", "true")
|
||||||
.doRun()
|
.doRun()
|
||||||
@ -395,14 +415,31 @@ class HsOfficeScenarioTests extends ScenarioTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Order(4090)
|
@Order(4080)
|
||||||
@Requires("Membership: M-3101000 - Test AG")
|
@Requires("Membership: M-3101000 - Test AG")
|
||||||
|
@Produces("Membership: M-3101000 - Test AG - cancelled")
|
||||||
void shouldCancelMembershipOfPartner() {
|
void shouldCancelMembershipOfPartner() {
|
||||||
new CancelMembership(scenarioTest)
|
new CancelMembership(scenarioTest)
|
||||||
.given("memberNumber", "M-3101000")
|
.given("memberNumber", "M-3101000")
|
||||||
.given("validTo", "2025-12-30")
|
.given("validTo", "2023-12-31")
|
||||||
.given("newStatus", "CANCELLED")
|
.given("newStatus", "CANCELLED")
|
||||||
.doRun();
|
.doRun()
|
||||||
|
.keep();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(4090)
|
||||||
|
@Requires("Membership: M-3101000 - Test AG - cancelled")
|
||||||
|
@Produces("Membership: M-3101001 - Test AG")
|
||||||
|
void shouldCreateSubsequentMembershipOfPartner() {
|
||||||
|
new CreateMembership(scenarioTest)
|
||||||
|
.given("partnerName", "Test AG")
|
||||||
|
.given("memberNumberSuffix", "01")
|
||||||
|
.given("validFrom", "2025-02-24")
|
||||||
|
.given("newStatus", "ACTIVE")
|
||||||
|
.given("membershipFeeBillable", "true")
|
||||||
|
.doRun()
|
||||||
|
.keep();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ public class DeleteDebitor extends UseCase<DeleteDebitor> {
|
|||||||
.given("vatCountryCode", "DE")
|
.given("vatCountryCode", "DE")
|
||||||
.given("vatBusiness", true)
|
.given("vatBusiness", true)
|
||||||
.given("vatReverseCharge", false)
|
.given("vatReverseCharge", false)
|
||||||
.given("defaultPrefix", "tsy"));
|
.given("defaultPrefix", "tsz"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.test;
|
package net.hostsharing.hsadminng.rbac.test;
|
||||||
|
|
||||||
import org.assertj.core.api.ObjectAssert;
|
import lombok.SneakyThrows;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.NestedExceptionUtils;
|
import org.springframework.core.NestedExceptionUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -78,9 +78,9 @@ public class JpaAttempt {
|
|||||||
public static class JpaResult<T> {
|
public static class JpaResult<T> {
|
||||||
|
|
||||||
private final T value;
|
private final T value;
|
||||||
private final RuntimeException exception;
|
private final Throwable exception;
|
||||||
|
|
||||||
private JpaResult(final T value, final RuntimeException exception) {
|
private JpaResult(final T value, final Throwable exception) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.exception = exception;
|
this.exception = exception;
|
||||||
}
|
}
|
||||||
@ -93,7 +93,7 @@ public class JpaAttempt {
|
|||||||
return new JpaResult<>(value, null);
|
return new JpaResult<>(value, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T> JpaResult<T> forException(final RuntimeException exception) {
|
public static <T> JpaResult<T> forException(final Throwable exception) {
|
||||||
return new JpaResult<>(null, exception);
|
return new JpaResult<>(null, exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,20 +105,23 @@ public class JpaAttempt {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectAssert<T> assertThatResult() {
|
public Throwable caughtException() {
|
||||||
assertSuccessful();
|
|
||||||
return assertThat(returnedValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
public RuntimeException caughtException() {
|
|
||||||
return exception;
|
return exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
public <E extends Throwable> E caughtException(final Class<E> expectedExceptionClass) {
|
||||||
public <E extends RuntimeException> E caughtException(final Class<E> expectedExceptionClass) {
|
//noinspection unchecked
|
||||||
|
return caughtException((E) exception, expectedExceptionClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <E extends Throwable> E caughtException(final Throwable exception, final Class<E> expectedExceptionClass) {
|
||||||
if (expectedExceptionClass.isAssignableFrom(exception.getClass())) {
|
if (expectedExceptionClass.isAssignableFrom(exception.getClass())) {
|
||||||
|
//noinspection unchecked
|
||||||
return (E) exception;
|
return (E) exception;
|
||||||
}
|
}
|
||||||
|
if(exception.getCause() != null && exception.getCause() != exception ) {
|
||||||
|
return caughtException(exception.getCause(), expectedExceptionClass);
|
||||||
|
}
|
||||||
throw new AssertionError("expected " + expectedExceptionClass + " but got " + exception);
|
throw new AssertionError("expected " + expectedExceptionClass + " but got " + exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +130,7 @@ public class JpaAttempt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void assertExceptionWithRootCauseMessage(
|
public void assertExceptionWithRootCauseMessage(
|
||||||
final Class<? extends RuntimeException> expectedExceptionClass,
|
final Class<? extends Throwable> expectedExceptionClass,
|
||||||
final String... expectedRootCauseMessages) {
|
final String... expectedRootCauseMessages) {
|
||||||
assertThat(wasSuccessful()).as("wasSuccessful").isFalse();
|
assertThat(wasSuccessful()).as("wasSuccessful").isFalse();
|
||||||
final String firstRootCauseMessageLine = firstRootCauseMessageLineOf(caughtException(expectedExceptionClass));
|
final String firstRootCauseMessageLine = firstRootCauseMessageLineOf(caughtException(expectedExceptionClass));
|
||||||
@ -136,11 +139,11 @@ public class JpaAttempt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public JpaResult<T> reThrowException() {
|
@SneakyThrows
|
||||||
|
public void reThrowException() {
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public JpaResult<T> assumeSuccessful() {
|
public JpaResult<T> assumeSuccessful() {
|
||||||
@ -158,9 +161,9 @@ public class JpaAttempt {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String firstRootCauseMessageLineOf(final RuntimeException exception) {
|
private String firstRootCauseMessageLineOf(final Throwable exception) {
|
||||||
final var rootCause = NestedExceptionUtils.getRootCause(exception);
|
final var rootCause = NestedExceptionUtils.getRootCause(exception);
|
||||||
return Optional.ofNullable(rootCause)
|
return Optional.ofNullable(rootCause != null ? rootCause : exception)
|
||||||
.map(Throwable::getMessage)
|
.map(Throwable::getMessage)
|
||||||
.map(message -> message.split("\\r|\\n|\\r\\n", 0)[0])
|
.map(message -> message.split("\\r|\\n|\\r\\n", 0)[0])
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user