From b36712076d2d60a91f4464fddc20bc2dae3831a4 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 28 Nov 2024 07:10:31 +0100 Subject: [PATCH] implement coop-asset-TRANSFER-transaction reversal (#125) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/125 Reviewed-by: Marc Sandlus --- ...OfficeCoopAssetsTransactionController.java | 143 ++-- .../HsOfficeCoopAssetsTransactionEntity.java | 8 +- .../hsadminng/lambda/WithNonNull.java | 11 + .../5120-hs-office-coopassets.sql | 44 +- ...opAssetsTransactionControllerRestTest.java | 651 +++++++++++++++++- .../scenarios/HsOfficeScenarioTests.java | 21 +- .../hs/office/scenarios/JsonOptional.java | 44 ++ .../hs/office/scenarios/PathAssertion.java | 13 +- .../hs/office/scenarios/ScenarioTest.java | 19 +- .../hs/office/scenarios/TestReport.java | 24 +- .../hs/office/scenarios/UseCase.java | 43 +- ...ateCoopAssetsRevertSimpleTransaction.java} | 8 +- ...teCoopAssetsRevertTransferTransaction.java | 59 ++ .../CreateCoopAssetsTransferTransaction.java | 5 +- .../hsadminng/lambda/WithNonNullUnitTest.java | 30 + .../hsadminng/rbac/test/JsonBuilder.java | 2 +- .../hsadminng/test/TestUuidGenerator.java | 3 + 17 files changed, 988 insertions(+), 140 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/lambda/WithNonNull.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/JsonOptional.java rename src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/{CreateCoopAssetsRevertTransaction.java => CreateCoopAssetsRevertSimpleTransaction.java} (71%) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransferTransaction.java create mode 100644 src/test/java/net/hostsharing/hsadminng/lambda/WithNonNullUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java index 24a1919c..8288d7c1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java @@ -27,11 +27,13 @@ import java.util.UUID; import java.util.function.BiConsumer; import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.REVERSAL; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.TRANSFER; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.CLEARING; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.DEPOSIT; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.DISBURSAL; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.LOSS; -import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.TRANSFER; +import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull; @RestController public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi { @@ -66,7 +68,10 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse fromValueDate, toValueDate); - final var resources = mapper.mapList(entities, HsOfficeCoopAssetsTransactionResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); + final var resources = mapper.mapList( + entities, + HsOfficeCoopAssetsTransactionResource.class, + ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(resources); } @@ -106,7 +111,11 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse if (result.isEmpty()) { return ResponseEntity.notFound().build(); } - return ResponseEntity.ok(mapper.map(result.get(), HsOfficeCoopAssetsTransactionResource.class)); + final var resource = mapper.map( + result.get(), + HsOfficeCoopAssetsTransactionResource.class, + ENTITY_TO_RESOURCE_POSTMAPPER); + return ResponseEntity.ok(resource); } @@ -131,7 +140,8 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse private static void validateCreditTransaction( final HsOfficeCoopAssetsTransactionInsertResource requestBody, final ArrayList violations) { - if (List.of(DISBURSAL, TRANSFER, CLEARING, LOSS).contains(requestBody.getTransactionType()) + if (List.of(DISBURSAL, HsOfficeCoopAssetsTransactionTypeResource.TRANSFER, CLEARING, LOSS) + .contains(requestBody.getTransactionType()) && requestBody.getAssetValue().signum() > 0) { violations.add("for %s, assetValue must be negative but is \"%.2f\"".formatted( requestBody.getTransactionType(), requestBody.getAssetValue())); @@ -147,57 +157,108 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse } } + // TODO.refa: this logic needs to get extracted to a service final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { resource.setMembershipUuid(entity.getMembership().getUuid()); resource.setMembershipMemberNumber(entity.getMembership().getTaggedMemberNumber()); - if (entity.getReversalAssetTx() != null) { - resource.getReversalAssetTx().setRevertedAssetTxUuid(entity.getUuid()); - resource.getReversalAssetTx().setMembershipUuid(entity.getMembership().getUuid()); - resource.getReversalAssetTx().setMembershipMemberNumber(entity.getTaggedMemberNumber()); - } + withNonNull( + resource.getReversalAssetTx(), reversalAssetTxResource -> { + reversalAssetTxResource.setMembershipUuid(entity.getMembership().getUuid()); + reversalAssetTxResource.setMembershipMemberNumber(entity.getTaggedMemberNumber()); + reversalAssetTxResource.setRevertedAssetTxUuid(entity.getUuid()); + withNonNull( + entity.getAdoptionAssetTx(), adoptionAssetTx -> + reversalAssetTxResource.setAdoptionAssetTxUuid(adoptionAssetTx.getUuid())); + withNonNull( + entity.getTransferAssetTx(), transferAssetTxResource -> + reversalAssetTxResource.setTransferAssetTxUuid(transferAssetTxResource.getUuid())); + }); - if (entity.getRevertedAssetTx() != null) { - resource.getRevertedAssetTx().setReversalAssetTxUuid(entity.getUuid()); - resource.getRevertedAssetTx().setMembershipUuid(entity.getMembership().getUuid()); - resource.getRevertedAssetTx().setMembershipMemberNumber(entity.getTaggedMemberNumber()); - } + withNonNull( + resource.getRevertedAssetTx(), revertAssetTxResource -> { + revertAssetTxResource.setMembershipUuid(entity.getMembership().getUuid()); + revertAssetTxResource.setMembershipMemberNumber(entity.getTaggedMemberNumber()); + revertAssetTxResource.setReversalAssetTxUuid(entity.getUuid()); + withNonNull( + entity.getRevertedAssetTx().getAdoptionAssetTx(), adoptionAssetTx -> + revertAssetTxResource.setAdoptionAssetTxUuid(adoptionAssetTx.getUuid())); + withNonNull( + entity.getRevertedAssetTx().getTransferAssetTx(), transferAssetTxResource -> + revertAssetTxResource.setTransferAssetTxUuid(transferAssetTxResource.getUuid())); + }); - if (entity.getAdoptionAssetTx() != null) { - resource.getAdoptionAssetTx().setTransferAssetTxUuid(entity.getUuid()); - resource.getAdoptionAssetTx().setMembershipUuid(entity.getAdoptionAssetTx().getMembership().getUuid()); - resource.getAdoptionAssetTx().setMembershipMemberNumber(entity.getAdoptionAssetTx().getTaggedMemberNumber()); - } + withNonNull( + resource.getAdoptionAssetTx(), adoptionAssetTxResource -> { + adoptionAssetTxResource.setMembershipUuid(entity.getAdoptionAssetTx().getMembership().getUuid()); + adoptionAssetTxResource.setMembershipMemberNumber(entity.getAdoptionAssetTx().getTaggedMemberNumber()); + adoptionAssetTxResource.setTransferAssetTxUuid(entity.getUuid()); + withNonNull( + entity.getAdoptionAssetTx().getReversalAssetTx(), reversalAssetTx -> + adoptionAssetTxResource.setReversalAssetTxUuid(reversalAssetTx.getUuid())); + }); - if (entity.getTransferAssetTx() != null) { - resource.getTransferAssetTx().setAdoptionAssetTxUuid(entity.getUuid()); - resource.getTransferAssetTx().setMembershipUuid(entity.getTransferAssetTx().getMembership().getUuid()); - resource.getTransferAssetTx().setMembershipMemberNumber(entity.getTransferAssetTx().getTaggedMemberNumber()); - } + withNonNull( + resource.getTransferAssetTx(), transferAssetTxResource -> { + resource.getTransferAssetTx().setMembershipUuid(entity.getTransferAssetTx().getMembership().getUuid()); + resource.getTransferAssetTx() + .setMembershipMemberNumber(entity.getTransferAssetTx().getTaggedMemberNumber()); + resource.getTransferAssetTx().setAdoptionAssetTxUuid(entity.getUuid()); + withNonNull( + entity.getTransferAssetTx().getReversalAssetTx(), reversalAssetTx -> + transferAssetTxResource.setReversalAssetTxUuid(reversalAssetTx.getUuid())); + }); }; + // TODO.refa: this logic needs to get extracted to a service final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { if (resource.getMembershipUuid() != null) { - final HsOfficeMembershipEntity membership = ofNullable(emw.find(HsOfficeMembershipEntity.class, resource.getMembershipUuid())) - .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] membership.uuid %s not found".formatted( + final HsOfficeMembershipEntity membership = ofNullable(emw.find( + HsOfficeMembershipEntity.class, + resource.getMembershipUuid())) + .orElseThrow(() -> new EntityNotFoundException("membership.uuid %s not found".formatted( resource.getMembershipUuid()))); entity.setMembership(membership); } - if (resource.getRevertedAssetTxUuid() != null) { + + if (entity.getTransactionType() == REVERSAL) { + if (resource.getRevertedAssetTxUuid() == null) { + throw new ValidationException("REVERSAL asset transaction requires revertedAssetTx.uuid"); + } final var revertedAssetTx = coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid()) - .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] revertedEntityUuid %s not found".formatted( + .orElseThrow(() -> new EntityNotFoundException("revertedAssetTx.uuid %s not found".formatted( resource.getRevertedAssetTxUuid()))); + revertedAssetTx.setReversalAssetTx(entity); entity.setRevertedAssetTx(revertedAssetTx); if (resource.getAssetValue().negate().compareTo(revertedAssetTx.getAssetValue()) != 0) { throw new ValidationException("given assetValue=" + resource.getAssetValue() + " but must be negative value from reverted asset tx: " + revertedAssetTx.getAssetValue()); } + + if (revertedAssetTx.getTransactionType() == TRANSFER) { + final var adoptionAssetTx = revertedAssetTx.getAdoptionAssetTx(); + final var adoptionReversalAssetTx = HsOfficeCoopAssetsTransactionEntity.builder() + .transactionType(REVERSAL) + .membership(adoptionAssetTx.getMembership()) + .revertedAssetTx(adoptionAssetTx) + .assetValue(adoptionAssetTx.getAssetValue().negate()) + .comment(resource.getComment()) + .reference(resource.getReference()) + .valueDate(resource.getValueDate()) + .build(); + adoptionAssetTx.setReversalAssetTx(adoptionReversalAssetTx); + adoptionReversalAssetTx.setRevertedAssetTx(adoptionAssetTx); + } } - final var adoptingMembership = determineAdoptingMembership(resource); - if (adoptingMembership != null) { - final var adoptingAssetTx = coopAssetsTransactionRepo.save(createAdoptingAssetTx(entity, adoptingMembership)); + if (resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER) { + final var adoptingMembership = determineAdoptingMembership(resource); + if ( entity.getMembership() == adoptingMembership) { + throw new ValidationException("transferring and adopting membership must be different, but both are " + + adoptingMembership.getTaggedMemberNumber()); + } + final var adoptingAssetTx = createAdoptingAssetTx(entity, adoptingMembership); entity.setAdoptionAssetTx(adoptingAssetTx); } }; @@ -206,11 +267,11 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse final var adoptingMembershipUuid = resource.getAdoptingMembershipUuid(); final var adoptingMembershipMemberNumber = resource.getAdoptingMembershipMemberNumber(); if (adoptingMembershipUuid != null && adoptingMembershipMemberNumber != null) { - throw new IllegalArgumentException( - // @formatter:off - resource.getTransactionType() == TRANSFER - ? "[400] either adoptingMembership.uuid or adoptingMembership.memberNumber can be given, not both" - : "[400] adoptingMembership.uuid and adoptingMembership.memberNumber must not be given for transactionType=" + throw new ValidationException( + // @formatter:off + resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER + ? "either adoptingMembership.uuid or adoptingMembership.memberNumber can be given, not both" + : "adoptingMembership.uuid and adoptingMembership.memberNumber must not be given for transactionType=" + resource.getTransactionType()); // @formatter:on } @@ -232,13 +293,9 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse + "' not found or not accessible"); } - if (resource.getTransactionType() == TRANSFER) { - throw new ValidationException( - "either adoptingMembership.uuid or adoptingMembership.memberNumber must be given for transactionType=" - + TRANSFER); - } - - return null; + throw new ValidationException( + "either adoptingMembership.uuid or adoptingMembership.memberNumber must be given for transactionType=" + + HsOfficeCoopAssetsTransactionTypeResource.TRANSFER); } private HsOfficeCoopAssetsTransactionEntity createAdoptingAssetTx( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java index 395c2895..b07bdebe 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java @@ -98,21 +98,21 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE private String comment; // Optionally, the UUID of the corresponding transaction for a reversal transaction. - @OneToOne(cascade = CascadeType.PERSIST) // TODO.impl: can probably be removed after office data migration + @OneToOne @JoinColumn(name = "revertedassettxuuid") private HsOfficeCoopAssetsTransactionEntity revertedAssetTx; // and the other way around - @OneToOne(mappedBy = "revertedAssetTx") + @OneToOne(mappedBy = "revertedAssetTx", cascade = CascadeType.PERSIST) private HsOfficeCoopAssetsTransactionEntity reversalAssetTx; // Optionally, the UUID of the corresponding transaction for a transfer transaction. - @OneToOne(cascade = CascadeType.PERSIST) // TODO.impl: can probably be removed after office data migration + @OneToOne(cascade = CascadeType.PERSIST) @JoinColumn(name = "assetadoptiontxuuid") private HsOfficeCoopAssetsTransactionEntity adoptionAssetTx; // and the other way around - @OneToOne(mappedBy = "adoptionAssetTx") + @OneToOne(mappedBy = "adoptionAssetTx", cascade = CascadeType.PERSIST) private HsOfficeCoopAssetsTransactionEntity transferAssetTx; @Override diff --git a/src/main/java/net/hostsharing/hsadminng/lambda/WithNonNull.java b/src/main/java/net/hostsharing/hsadminng/lambda/WithNonNull.java new file mode 100644 index 00000000..358f5319 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/lambda/WithNonNull.java @@ -0,0 +1,11 @@ +package net.hostsharing.hsadminng.lambda; + +import java.util.function.Consumer; + +public class WithNonNull { + public static void withNonNull(final T target, final Consumer code) { + if (target != null ) { + code.accept(target); + } + } +} diff --git a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql index 37c2affc..524e620c 100644 --- a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql +++ b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql @@ -35,21 +35,41 @@ create table if not exists hs_office.coopassettx --changeset michael.hoennig:hs-office-coopassets-BUSINESS-RULES endDelimiter:--// -- ---------------------------------------------------------------------------- -alter table hs_office.coopassettx - add constraint reversal_asset_tx_must_have_reverted_asset_tx - check (transactionType <> 'REVERSAL' or revertedAssetTxUuid is not null); +-- Not as CHECK constraints because those cannot be deferrable, +-- but we need these constraints deferrable because the rows are linked to each other. -alter table hs_office.coopassettx - add constraint non_reversal_asset_tx_must_not_have_reverted_asset_tx - check (transactionType = 'REVERSAL' or revertedAssetTxUuid is null or transactionType = 'REVERSAL'); +CREATE OR REPLACE FUNCTION validate_transaction_type() + RETURNS TRIGGER AS $$ +BEGIN + -- REVERSAL transactions must have revertedAssetTxUuid + IF NEW.transactionType = 'REVERSAL' AND NEW.revertedAssetTxUuid IS NULL THEN + RAISE EXCEPTION 'REVERSAL transactions must have revertedAssetTxUuid'; + END IF; -alter table hs_office.coopassettx - add constraint transfer_asset_tx_must_have_adopted_asset_tx - check (transactionType <> 'TRANSFER' or assetAdoptionTxUuid is not null); + -- Non-REVERSAL transactions must not have revertedAssetTxUuid + IF NEW.transactionType != 'REVERSAL' AND NEW.revertedAssetTxUuid IS NOT NULL THEN + RAISE EXCEPTION 'Non-REVERSAL transactions must not have revertedAssetTxUuid'; + END IF; + + -- TRANSFER transactions must have assetAdoptionTxUuid + IF NEW.transactionType = 'TRANSFER' AND NEW.assetAdoptionTxUuid IS NULL THEN + RAISE EXCEPTION 'TRANSFER transactions must have assetAdoptionTxUuid'; + END IF; + + -- Non-TRANSFER transactions must not have assetAdoptionTxUuid + IF NEW.transactionType != 'TRANSFER' AND NEW.assetAdoptionTxUuid IS NOT NULL THEN + RAISE EXCEPTION 'Non-TRANSFER transactions must not have assetAdoptionTxUuid'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Attach the trigger to the table +CREATE TRIGGER enforce_transaction_constraints + AFTER INSERT OR UPDATE ON hs_office.coopassettx + FOR EACH ROW EXECUTE FUNCTION validate_transaction_type(); -alter table hs_office.coopassettx - add constraint non_transfer_asset_tx_must_not_have_adopted_asset_tx - check (transactionType = 'TRANSFER' or assetAdoptionTxUuid is null); --// -- ============================================================================ diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java index 1fdbe4f0..4e6d5d51 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java @@ -10,6 +10,7 @@ import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.rbac.test.JsonBuilder; import net.hostsharing.hsadminng.test.TestUuidGenerator; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.runner.RunWith; @@ -24,12 +25,20 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.function.Function; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.ADOPTION; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.DEPOSIT; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.DISBURSAL; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.REVERSAL; +import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.TRANSFER; import static net.hostsharing.hsadminng.rbac.test.JsonBuilder.jsonObject; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -42,7 +51,13 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @RunWith(SpringRunner.class) class HsOfficeCoopAssetsTransactionControllerRestTest { - private static final UUID UNAVAILABLE_MEMBERSHIP_UUID = TestUuidGenerator.use(0); + // If you need to run just a single test-case in this data-driven test-method, set SINGLE_TEST_CASE_EXECUTION to true! + // There is a test which fails if single test-case execution active to avoid merging this to master. + private static final boolean SINGLE_TEST_CASE_EXECUTION = false; + + private static final int DYNAMIC_UUID_START_INDEX = 13; + + private static final UUID UNAVAILABLE_UUID = TestUuidGenerator.use(0); private static final String UNAVAILABLE_MEMBER_NUMBER = "M-1234699"; private static final UUID ORIGIN_MEMBERSHIP_UUID = TestUuidGenerator.use(1); @@ -65,9 +80,11 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { .memberNumberSuffix(suffixOf(AVAILABLE_TARGET_MEMBER_NUMBER)) .build(); - // the following refs might change if impl changes - private static final UUID NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID = TestUuidGenerator.ref(4); - private static final UUID NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID = TestUuidGenerator.ref(5); + // The following refs depend on the implementation of the respective implementation and might change if it changes. + // The same TestUuidGenerator.ref(#) does NOT mean the UUIDs refer to the same entity, + // its rather coincidence because different test-cases have different execution paths in the production code. + private static final UUID NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID = TestUuidGenerator.ref(DYNAMIC_UUID_START_INDEX); + private static final UUID NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID = TestUuidGenerator.ref(DYNAMIC_UUID_START_INDEX); private static final UUID SOME_EXISTING_LOSS_ASSET_TX_UUID = TestUuidGenerator.use(3); public final HsOfficeCoopAssetsTransactionEntity SOME_EXISTING_LOSS_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder() @@ -80,6 +97,402 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { .valueDate(LocalDate.parse("2024-10-15")) .build(); + private static final UUID SOME_EXISTING_TRANSFER_ASSET_TX_UUID = TestUuidGenerator.use(4); + public final HsOfficeCoopAssetsTransactionEntity SOME_EXISTING_TRANSFER_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_EXISTING_TRANSFER_ASSET_TX_UUID) + .membership(ORIGIN_TARGET_MEMBER_ENTITY) + .transactionType(HsOfficeCoopAssetsTransactionType.TRANSFER) + .assetValue(BigDecimal.valueOf(-256)) + .reference("some transfer asset tx ref") + .comment("some transfer asset tx comment") + .valueDate(LocalDate.parse("2024-10-15")) + .build(); + + private static final UUID SOME_EXISTING_ADOPTION_ASSET_TX_UUID = TestUuidGenerator.use(5); + public final HsOfficeCoopAssetsTransactionEntity SOME_EXISTING_ADOPTION_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_EXISTING_ADOPTION_ASSET_TX_UUID) + .membership(ORIGIN_TARGET_MEMBER_ENTITY) + .transactionType(HsOfficeCoopAssetsTransactionType.TRANSFER) + .assetValue(SOME_EXISTING_TRANSFER_ASSET_TX_ENTITY.getAssetValue().negate()) + .reference("some adoption asset tx ref") + .comment("some adoption asset tx comment") + .valueDate(LocalDate.parse("2024-10-15")) + .transferAssetTx(SOME_EXISTING_TRANSFER_ASSET_TX_ENTITY) + .build(); + { + SOME_EXISTING_TRANSFER_ASSET_TX_ENTITY.setAdoptionAssetTx(SOME_EXISTING_ADOPTION_ASSET_TX_ENTITY); + } + + private final static UUID SOME_REVERTED_DISBURSAL_ASSET_TX_UUID = TestUuidGenerator.use(7); + private final static UUID SOME_DISBURSAL_REVERSAL_ASSET_TX_UUID = TestUuidGenerator.use(8); + private final HsOfficeCoopAssetsTransactionEntity SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_REVERTED_DISBURSAL_ASSET_TX_UUID) + .membership(ORIGIN_TARGET_MEMBER_ENTITY) + .transactionType(DISBURSAL) + .assetValue(BigDecimal.valueOf(-128.00)) + .valueDate(LocalDate.parse("2024-10-15")) + .reference("some disbursal") + .comment("some disbursal to get reverted") + .reversalAssetTx( + HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_DISBURSAL_REVERSAL_ASSET_TX_UUID) + .membership(ORIGIN_TARGET_MEMBER_ENTITY) + .transactionType(REVERSAL) + .assetValue(BigDecimal.valueOf(128.00)) + .valueDate(LocalDate.parse("2024-10-20")) + .reference("some reversal") + .comment("some reversal of a disbursal asset tx") + .build() + ) + .build(); + { + SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY.getReversalAssetTx().setRevertedAssetTx(SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY); + } + + private final static UUID SOME_REVERTED_TRANSFER_ASSET_TX_UUID = TestUuidGenerator.use(9); + private final static UUID SOME_TRANSFER_REVERSAL_ASSET_TX_UUID = TestUuidGenerator.use(10); + private final static UUID SOME_REVERTED_ADOPTION_ASSET_TX_UUID = TestUuidGenerator.use(11); + private final static UUID SOME_ADOPTION_REVERSAL_ASSET_TX_UUID = TestUuidGenerator.use(12); + final HsOfficeCoopAssetsTransactionEntity SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_REVERTED_TRANSFER_ASSET_TX_UUID) + .membership(ORIGIN_TARGET_MEMBER_ENTITY) + .transactionType(TRANSFER) + .assetValue(BigDecimal.valueOf(-1024)) + .valueDate(LocalDate.parse("2024-11-10")) + .reference("some transfer") + .comment("some transfer to get reverted") + .adoptionAssetTx( + HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_REVERTED_ADOPTION_ASSET_TX_UUID) + .membership(AVAILABLE_MEMBER_ENTITY) + .transactionType(ADOPTION) + .assetValue(BigDecimal.valueOf(1024)) + .valueDate(LocalDate.parse("2024-11-10")) + .reference("related adoption") + .comment("some reversal of a transfer asset tx") + .reversalAssetTx( + HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_ADOPTION_REVERSAL_ASSET_TX_UUID) + .membership(AVAILABLE_MEMBER_ENTITY) + .transactionType(REVERSAL) + .assetValue(BigDecimal.valueOf(1024)) + .valueDate(LocalDate.parse("2024-11-11")) + .reference("some reversal") + .comment("some adoption asset tx reversal") + .build() + ) + .build() + ) + .reversalAssetTx( + HsOfficeCoopAssetsTransactionEntity.builder() + .uuid(SOME_TRANSFER_REVERSAL_ASSET_TX_UUID) + .membership(ORIGIN_TARGET_MEMBER_ENTITY) + .transactionType(REVERSAL) + .assetValue(BigDecimal.valueOf(1024)) + .valueDate(LocalDate.parse("2024-11-11")) + .reference("some transfer") + .comment("some transfer asset tx reversal") + .build() + ) + .build(); + { + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getAdoptionAssetTx() + .setTransferAssetTx(SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY); + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getReversalAssetTx() + .setRevertedAssetTx(SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY); + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getAdoptionAssetTx().getReversalAssetTx() + .setRevertedAssetTx(SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getAdoptionAssetTx()); + } + + private static final String EXPECTED_RESULT_FROM_GET_SINGLE = """ + { + "uuid": "99999999-9999-9999-9999-999999999999", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "TRANSFER", + "assetValue": -1024, + "valueDate": "2024-11-10", + "reference": "some transfer", + "comment": "some transfer to get reverted", + "adoptionAssetTx": { + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "membership.uuid": "22222222-2222-2222-2222-222222222222", + "membership.memberNumber": "M-1234500", + "transactionType": "ADOPTION", + "assetValue": 1024, + "valueDate": "2024-11-10", + "reference": "related adoption", + "comment": "some reversal of a transfer asset tx", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": "99999999-9999-9999-9999-999999999999", + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "cccccccc-cccc-cccc-cccc-cccccccccccc" + } + } + """; + + + private static final String EXPECTED_RESULT_FROM_GET_LIST = """ + [ + { + "uuid": "33333333-3333-3333-3333-333333333333", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "LOSS", + "assetValue": -64, + "valueDate": "2024-10-15", + "reference": "some loss asset tx ref", + "comment": "some loss asset tx comment", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": null, + "reversalAssetTx": null + }, + { + "uuid": "44444444-4444-4444-4444-444444444444", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "TRANSFER", + "assetValue": -256, + "valueDate": "2024-10-15", + "reference": "some transfer asset tx ref", + "comment": "some transfer asset tx comment", + "adoptionAssetTx": { + "uuid": "55555555-5555-5555-5555-555555555555", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "TRANSFER", + "assetValue": 256, + "valueDate": "2024-10-15", + "reference": "some adoption asset tx ref", + "comment": "some adoption asset tx comment", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": "44444444-4444-4444-4444-444444444444", + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": null + }, + "transferAssetTx": null, + "revertedAssetTx": null, + "reversalAssetTx": null + }, + { + "uuid": "55555555-5555-5555-5555-555555555555", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "TRANSFER", + "assetValue": 256, + "valueDate": "2024-10-15", + "reference": "some adoption asset tx ref", + "comment": "some adoption asset tx comment", + "adoptionAssetTx": null, + "transferAssetTx": { + "uuid": "44444444-4444-4444-4444-444444444444", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "TRANSFER", + "assetValue": -256, + "valueDate": "2024-10-15", + "reference": "some transfer asset tx ref", + "comment": "some transfer asset tx comment", + "adoptionAssetTx.uuid": "55555555-5555-5555-5555-555555555555", + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": null + }, + "revertedAssetTx": null, + "reversalAssetTx": null + }, + { + "uuid": "77777777-7777-7777-7777-777777777777", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "DISBURSAL", + "assetValue": -128.0, + "valueDate": "2024-10-15", + "reference": "some disbursal", + "comment": "some disbursal to get reverted", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": null, + "reversalAssetTx": { + "uuid": "88888888-8888-8888-8888-888888888888", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "REVERSAL", + "assetValue": 128.0, + "valueDate": "2024-10-20", + "reference": "some reversal", + "comment": "some reversal of a disbursal asset tx", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": "77777777-7777-7777-7777-777777777777", + "reversalAssetTx.uuid": null + } + }, + { + "uuid": "88888888-8888-8888-8888-888888888888", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "REVERSAL", + "assetValue": 128.0, + "valueDate": "2024-10-20", + "reference": "some reversal", + "comment": "some reversal of a disbursal asset tx", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": { + "uuid": "77777777-7777-7777-7777-777777777777", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "DISBURSAL", + "assetValue": -128.0, + "valueDate": "2024-10-15", + "reference": "some disbursal", + "comment": "some disbursal to get reverted", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "88888888-8888-8888-8888-888888888888" + }, + "reversalAssetTx": null + }, + { + "uuid": "99999999-9999-9999-9999-999999999999", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "TRANSFER", + "assetValue": -1024, + "valueDate": "2024-11-10", + "reference": "some transfer", + "comment": "some transfer to get reverted", + "adoptionAssetTx": { + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "membership.uuid": "22222222-2222-2222-2222-222222222222", + "membership.memberNumber": "M-1234500", + "transactionType": "ADOPTION", + "assetValue": 1024, + "valueDate": "2024-11-10", + "reference": "related adoption", + "comment": "some reversal of a transfer asset tx", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": "99999999-9999-9999-9999-999999999999", + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "cccccccc-cccc-cccc-cccc-cccccccccccc" + }, + "transferAssetTx": null, + "revertedAssetTx": null, + "reversalAssetTx": { + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "REVERSAL", + "assetValue": 1024, + "valueDate": "2024-11-11", + "reference": "some transfer", + "comment": "some transfer asset tx reversal", + "adoptionAssetTx.uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": "99999999-9999-9999-9999-999999999999", + "reversalAssetTx.uuid": null + } + }, + { + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "membership.uuid": "22222222-2222-2222-2222-222222222222", + "membership.memberNumber": "M-1234500", + "transactionType": "ADOPTION", + "assetValue": 1024, + "valueDate": "2024-11-10", + "reference": "related adoption", + "comment": "some reversal of a transfer asset tx", + "adoptionAssetTx": null, + "transferAssetTx": { + "uuid": "77777777-7777-7777-7777-777777777777", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "DISBURSAL", + "assetValue": -128.0, + "valueDate": "2024-10-15", + "reference": "some disbursal", + "comment": "some disbursal to get reverted", + "adoptionAssetTx.uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "88888888-8888-8888-8888-888888888888" + }, + "revertedAssetTx": null, + "reversalAssetTx": { + "uuid": "cccccccc-cccc-cccc-cccc-cccccccccccc", + "membership.uuid": "22222222-2222-2222-2222-222222222222", + "membership.memberNumber": "M-1234500", + "transactionType": "REVERSAL", + "assetValue": 1024, + "valueDate": "2024-11-11", + "reference": "some reversal", + "comment": "some adoption asset tx reversal", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": "77777777-7777-7777-7777-777777777777", + "revertedAssetTx.uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "reversalAssetTx.uuid": null + } + }, + { + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "REVERSAL", + "assetValue": 1024, + "valueDate": "2024-11-11", + "reference": "some transfer", + "comment": "some transfer asset tx reversal", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": { + "uuid": "77777777-7777-7777-7777-777777777777", + "membership.uuid": "11111111-1111-1111-1111-111111111111", + "membership.memberNumber": "M-1111100", + "transactionType": "DISBURSAL", + "assetValue": -128.0, + "valueDate": "2024-10-15", + "reference": "some disbursal", + "comment": "some disbursal to get reverted", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + }, + "reversalAssetTx": null + }, + { + "uuid": "cccccccc-cccc-cccc-cccc-cccccccccccc", + "membership.uuid": "22222222-2222-2222-2222-222222222222", + "membership.memberNumber": "M-1234500", + "transactionType": "REVERSAL", + "assetValue": 1024, + "valueDate": "2024-11-11", + "reference": "some reversal", + "comment": "some adoption asset tx reversal", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": { + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "membership.uuid": "22222222-2222-2222-2222-222222222222", + "membership.memberNumber": "M-1234500", + "transactionType": "ADOPTION", + "assetValue": 1024, + "valueDate": "2024-11-10", + "reference": "related adoption", + "comment": "some reversal of a transfer asset tx", + "adoptionAssetTx.uuid": null, + "transferAssetTx.uuid": "77777777-7777-7777-7777-777777777777", + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "cccccccc-cccc-cccc-cccc-cccccccccccc" + }, + "reversalAssetTx": null + } + ] + """; + @Autowired MockMvc mockMvc; @@ -117,6 +530,44 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { requestBody -> requestBody.without("membership.uuid"), "[membershipUuid must not be null but is \"null\"]"), // TODO.impl: should be membership.uuid, Spring validation-problem? + MEMBERSHIP_UUID_NOT_FOUND_OR_NOT_ACCESSIBLE( + requestBody -> requestBody.with("membership.uuid", UNAVAILABLE_UUID), + "membership.uuid " + UNAVAILABLE_UUID + " not found"), + + MEMBERSHIP_UUID_AND_MEMBER_NUMBER_MUST_NOT_BE_GIVEN_BOTH( + requestBody -> requestBody + .with("transactionType", TRANSFER.name()) + .with("assetValue", "-128.00") + .with("adoptingMembership.uuid", UNAVAILABLE_UUID) + .with("adoptingMembership.memberNumber", UNAVAILABLE_MEMBER_NUMBER), + "either adoptingMembership.uuid or adoptingMembership.memberNumber can be given, not both"), + + MEMBERSHIP_UUID_OR_MEMBER_NUMBER_MUST_BE_GIVEN( + requestBody -> requestBody + .with("transactionType", TRANSFER) + .with("assetValue", "-128.00"), + "either adoptingMembership.uuid or adoptingMembership.memberNumber must be given for transactionType=TRANSFER"), + + REVERSAL_ASSET_TRANSACTION_REQUIRES_REVERTED_ASSET_TX_UUID( + requestBody -> requestBody + .with("transactionType", REVERSAL) + .with("assetValue", "-128.00"), + "REVERSAL asset transaction requires revertedAssetTx.uuid"), + + REVERSAL_ASSET_TRANSACTION_REQUIRES_AVAILABLE_REVERTED_ASSET_TX_UUID( + requestBody -> requestBody + .with("transactionType", REVERSAL) + .with("assetValue", "-128.00") + .with("revertedAssetTx.uuid", UNAVAILABLE_UUID), + "revertedAssetTx.uuid " + UNAVAILABLE_UUID + " not found"), + + REVERSAL_ASSET_TRANSACTION_MUST_NEGATE_VALUE_OF_REVERTED_ASSET_TX( + requestBody -> requestBody + .with("transactionType", REVERSAL) + .with("assetValue", "128.00") + .with("revertedAssetTx.uuid", SOME_EXISTING_LOSS_ASSET_TX_UUID), + "given assetValue=128.00 but must be negative value from reverted asset tx: -64"), + TRANSACTION_TYPE_MISSING( requestBody -> requestBody.without("transactionType"), "[transactionType must not be null but is \"null\"]"), @@ -127,35 +578,40 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { ASSETS_VALUE_FOR_DEPOSIT_MUST_BE_POSITIVE( requestBody -> requestBody - .with("transactionType", "DEPOSIT") + .with("transactionType", DEPOSIT) .with("assetValue", -64.00), "[for DEPOSIT, assetValue must be positive but is \"-64.00\"]"), ASSETS_VALUE_FOR_DISBURSAL_MUST_BE_NEGATIVE( requestBody -> requestBody - .with("transactionType", "DISBURSAL") + .with("transactionType", DISBURSAL) .with("assetValue", 64.00), "[for DISBURSAL, assetValue must be negative but is \"64.00\"]"), - //TODO: other transaction types + ADOPTING_MEMBERSHIP_MUST_NOT_BE_THE_SAME( + requestBody -> requestBody + .with("transactionType", TRANSFER) + .with("assetValue", -64.00) + .with("adoptingMembership.uuid", ORIGIN_MEMBERSHIP_UUID), + "transferring and adopting membership must be different, but both are M-1111100"), ADOPTING_MEMBERSHIP_NUMBER_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE( requestBody -> requestBody - .with("transactionType", "TRANSFER") + .with("transactionType", TRANSFER) .with("assetValue", -64.00) .with("adoptingMembership.memberNumber", UNAVAILABLE_MEMBER_NUMBER), "adoptingMembership.memberNumber='M-1234699' not found or not accessible"), ADOPTING_MEMBERSHIP_UUID_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE( requestBody -> requestBody - .with("transactionType", "TRANSFER") + .with("transactionType", TRANSFER) .with("assetValue", -64.00) - .with("adoptingMembership.uuid", UNAVAILABLE_MEMBERSHIP_UUID.toString()), - "adoptingMembership.uuid='" + UNAVAILABLE_MEMBERSHIP_UUID + "' not found or not accessible"), + .with("adoptingMembership.uuid", UNAVAILABLE_UUID), + "adoptingMembership.uuid='" + UNAVAILABLE_UUID + "' not found or not accessible"), ASSETS_VALUE_MUST_NOT_BE_NULL( requestBody -> requestBody - .with("transactionType", "REVERSAL") + .with("transactionType", REVERSAL) .with("assetValue", 0.00), "[assetValue must not be 0 but is \"0.00\"]"), @@ -190,8 +646,10 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { @EnumSource(BadRequestTestCases.class) void respondWithBadRequest(final BadRequestTestCases testCase) throws Exception { // HOWTO: run just a single test-case in a data-driven test-method - // org.assertj.core.api.Assumptions.assumeThat( - // testCase == ADOPTING_MEMBERSHIP_NUMBER_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE).isTrue(); + // - set SINGLE_TEST_CASE_EXECUTION to true - see above + // - select the test case enum value you want to run + assumeThat(!SINGLE_TEST_CASE_EXECUTION || + testCase == BadRequestTestCases.ADOPTING_MEMBERSHIP_MUST_NOT_BE_THE_SAME).isTrue(); // when mockMvc.perform(MockMvcRequestBuilders @@ -202,9 +660,9 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { .accept(MediaType.APPLICATION_JSON)) // then - .andExpect(jsonPath("message", is("ERROR: [400] " + testCase.expectedErrorMessage))) .andExpect(jsonPath("statusCode", is(400))) .andExpect(jsonPath("statusPhrase", is("Bad Request"))) + .andExpect(jsonPath("message", is("ERROR: [400] " + testCase.expectedErrorMessage))) .andExpect(status().is4xxClientError()); } @@ -212,28 +670,38 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { REVERTING_SIMPLE_ASSET_TRANSACTION( requestBody -> requestBody - .with("transactionType", "REVERSAL") + .with("transactionType", REVERSAL) .with("assetValue", "64.00") .with("valueDate", "2024-10-15") - .with("reference", "reversal ref") - .with("comment", "reversal comment") - .with("revertedAssetTx.uuid", SOME_EXISTING_LOSS_ASSET_TX_UUID.toString()), - Expected.REVERT_RESPONSE), + .with("reference", "reversal of loss ref") + .with("comment", "reversal of loss asset tx comment") + .with("revertedAssetTx.uuid", SOME_EXISTING_LOSS_ASSET_TX_UUID), + Expected.REVERT_LOSS_RESPONSE), TRANSFER_TO_GIVEN_AVAILABLE_MEMBERSHIP_NUMBER( requestBody -> requestBody - .with("transactionType", "TRANSFER") + .with("transactionType", TRANSFER) .with("assetValue", -64.00) .with("adoptingMembership.memberNumber", AVAILABLE_TARGET_MEMBER_NUMBER), Expected.TRANSFER_RESPONSE), TRANSFER_TO_GIVEN_AVAILABLE_MEMBERSHIP_UUID( requestBody -> requestBody - .with("transactionType", "TRANSFER") + .with("transactionType", TRANSFER) .with("assetValue", -64.00) - .with("membership.uuid", ORIGIN_MEMBERSHIP_UUID.toString()) - .with("adoptingMembership.uuid", AVAILABLE_TARGET_MEMBERSHIP_UUID.toString()), - Expected.TRANSFER_RESPONSE); + .with("membership.uuid", ORIGIN_MEMBERSHIP_UUID) + .with("adoptingMembership.uuid", AVAILABLE_TARGET_MEMBERSHIP_UUID), + Expected.TRANSFER_RESPONSE), + + REVERTING_TRANSFER_ASSET_TRANSACTION_IMPLICITLY_REVERTS_ADOPTING_ASSET_TRANSACTION( + requestBody -> requestBody + .with("transactionType", REVERSAL) + .with("assetValue", "256.00") + .with("valueDate", "2024-10-15") + .with("reference", "reversal of transfer ref") + .with("comment", "reversal of transfer asset tx comment") + .with("revertedAssetTx.uuid", SOME_EXISTING_TRANSFER_ASSET_TX_UUID), + Expected.REVERT_TRANSFER_RESPONSE); private final Function givenBodyTransformation; private final String expectedResponseBody; @@ -251,7 +719,7 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { private static class Expected { - public static final String REVERT_RESPONSE = """ + public static final String REVERT_LOSS_RESPONSE = """ { "uuid": "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}", "membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}", @@ -259,9 +727,10 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { "transactionType": "REVERSAL", "assetValue": 64.00, "valueDate": "2024-10-15", - "reference": "reversal ref", - "comment": "reversal comment", + "reference": "reversal of loss ref", + "comment": "reversal of loss asset tx comment", "adoptionAssetTx": null, + "reversalAssetTx": null, "transferAssetTx": null, "revertedAssetTx": { "uuid": "%{SOME_EXISTING_LOSS_ASSET_TX_UUID}", @@ -279,7 +748,9 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { } } """ - .replace("%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}", NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID.toString()) + .replace( + "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}", + NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID.toString()) .replace("%{ORIGIN_MEMBERSHIP_UUID}", ORIGIN_MEMBERSHIP_UUID.toString()) .replace("%{ORIGIN_MEMBER_NUMBER}", ORIGIN_MEMBER_NUMBER) .replace("%{SOME_EXISTING_LOSS_ASSET_TX_UUID}", SOME_EXISTING_LOSS_ASSET_TX_UUID.toString()); @@ -303,20 +774,57 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { "reversalAssetTx": null } """ - .replace("%{NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID}", NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID.toString()) + .replace( + "%{NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID}", + NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID.toString()) .replace("%{ORIGIN_MEMBERSHIP_UUID}", ORIGIN_MEMBERSHIP_UUID.toString()) .replace("%{ORIGIN_MEMBER_NUMBER}", ORIGIN_MEMBER_NUMBER) .replace("%{AVAILABLE_MEMBERSHIP_UUID}", AVAILABLE_TARGET_MEMBERSHIP_UUID.toString()) .replace("%{AVAILABLE_TARGET_MEMBER_NUMBER}", AVAILABLE_TARGET_MEMBER_NUMBER); + + public static final String REVERT_TRANSFER_RESPONSE = """ + { + "uuid": "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}", + "membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}", + "membership.memberNumber": "%{ORIGIN_MEMBER_NUMBER}", + "transactionType": "REVERSAL", + "assetValue": 256.00, + "valueDate": "2024-10-15", + "reference": "reversal of transfer ref", + "comment": "reversal of transfer asset tx comment", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": { + "uuid": "%{SOME_EXISTING_TRANSFER_ASSET_TX_UUID}", + "membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}", + "membership.memberNumber": "%{ORIGIN_MEMBER_NUMBER}", + "transactionType": "TRANSFER", + "assetValue": -256.00, + "valueDate": "2024-10-15", + "reference": "some transfer asset tx ref", + "comment": "some transfer asset tx comment", + "adoptionAssetTx.uuid": "%{SOME_EXISTING_ADOPTION_ASSET_TX_UUID}", + "transferAssetTx.uuid": null, + "revertedAssetTx.uuid": null, + "reversalAssetTx.uuid": "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}" + } + } + """ + .replace( + "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}", + NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID.toString()) + .replace("%{ORIGIN_MEMBERSHIP_UUID}", ORIGIN_MEMBERSHIP_UUID.toString()) + .replace("%{ORIGIN_MEMBER_NUMBER}", ORIGIN_MEMBER_NUMBER) + .replace("%{SOME_EXISTING_TRANSFER_ASSET_TX_UUID}", SOME_EXISTING_TRANSFER_ASSET_TX_UUID.toString()) + .replace("%{SOME_EXISTING_ADOPTION_ASSET_TX_UUID}", SOME_EXISTING_ADOPTION_ASSET_TX_UUID.toString()); } } @ParameterizedTest @EnumSource(SuccessfullyCreatedTestCases.class) void respondWithSuccessfullyCreated(final SuccessfullyCreatedTestCases testCase) throws Exception { - // uncomment, if you need to run just a single test-case in this data-driven test-method - // org.assertj.core.api.Assumptions.assumeThat( - // testCase == ADOPTING_MEMBERSHIP_UUID_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE).isTrue(); + assumeThat(!SINGLE_TEST_CASE_EXECUTION || + testCase == SuccessfullyCreatedTestCases.REVERTING_TRANSFER_ASSET_TRANSACTION_IMPLICITLY_REVERTS_ADOPTING_ASSET_TRANSACTION).isTrue(); // when mockMvc.perform(MockMvcRequestBuilders @@ -331,12 +839,77 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { .andExpect(jsonPath("$", lenientlyEquals(testCase.expectedResponseBody))); } + @Test + void getSingleGeneratesProperJsonForAvailableUuid() throws Exception { + // given + when(coopAssetsTransactionRepo.findByUuid(SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getUuid())) + .thenReturn(Optional.of(SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY)); + + // when + mockMvc.perform(MockMvcRequestBuilders + .get("/api/hs/office/coopassetstransactions/" + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getUuid()) + .header("current-subject", "superuser-alex@hostsharing.net") + .contentType(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$", lenientlyEquals(EXPECTED_RESULT_FROM_GET_SINGLE))); + } + + @Test + void getSingleGeneratesNotFoundForUnavailableUuid() throws Exception { + // given + when(coopAssetsTransactionRepo.findByUuid(UNAVAILABLE_UUID)).thenReturn(Optional.empty()); + + // when + mockMvc.perform(MockMvcRequestBuilders + .get("/api/hs/office/coopassetstransactions/" + UNAVAILABLE_UUID) + .header("current-subject", "superuser-alex@hostsharing.net") + .contentType(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().isNotFound()); + } + + @Test + void getListGeneratesProperJson() throws Exception { + // given + when(coopAssetsTransactionRepo.findCoopAssetsTransactionByOptionalMembershipUuidAndDateRange(null, null, null)) + .thenReturn(List.of( + SOME_EXISTING_LOSS_ASSET_TX_ENTITY, + SOME_EXISTING_TRANSFER_ASSET_TX_ENTITY, + SOME_EXISTING_ADOPTION_ASSET_TX_ENTITY, + SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY, + SOME_REVERTED_DISBURSAL_ASSET_TX_ENTITY.getReversalAssetTx(), + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY, + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getAdoptionAssetTx(), + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getReversalAssetTx(), + SOME_REVERTED_TRANSFER_ASSET_TX_ENTITY.getAdoptionAssetTx().getReversalAssetTx() + )); + + // when + mockMvc.perform(MockMvcRequestBuilders + .get("/api/hs/office/coopassetstransactions") + .header("current-subject", "superuser-alex@hostsharing.net") + .contentType(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$", lenientlyEquals(EXPECTED_RESULT_FROM_GET_LIST))); + } + + @Test + void singleTestCaseExecutionMustBeDisabled() { + assertThat(SINGLE_TEST_CASE_EXECUTION).isFalse(); + } + @BeforeEach void initMocks() { - TestUuidGenerator.start(4); + TestUuidGenerator.start(DYNAMIC_UUID_START_INDEX); when(emw.find(eq(HsOfficeMembershipEntity.class), eq(ORIGIN_MEMBERSHIP_UUID))).thenReturn(ORIGIN_TARGET_MEMBER_ENTITY); - when(emw.find(eq(HsOfficeMembershipEntity.class), eq(AVAILABLE_TARGET_MEMBERSHIP_UUID))).thenReturn(AVAILABLE_MEMBER_ENTITY); + when(emw.find(eq(HsOfficeMembershipEntity.class), eq(AVAILABLE_TARGET_MEMBERSHIP_UUID))).thenReturn( + AVAILABLE_MEMBER_ENTITY); final var availableMemberNumber = Integer.valueOf(AVAILABLE_TARGET_MEMBER_NUMBER.substring("M-".length())); when(membershipRepo.findMembershipByMemberNumber(eq(availableMemberNumber))).thenReturn(AVAILABLE_MEMBER_ENTITY); @@ -346,6 +919,10 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { when(coopAssetsTransactionRepo.findByUuid(SOME_EXISTING_LOSS_ASSET_TX_UUID)) .thenReturn(Optional.of(SOME_EXISTING_LOSS_ASSET_TX_ENTITY)); + when(coopAssetsTransactionRepo.findByUuid(SOME_EXISTING_TRANSFER_ASSET_TX_UUID)) + .thenReturn(Optional.of(SOME_EXISTING_TRANSFER_ASSET_TX_ENTITY)); + when(coopAssetsTransactionRepo.findByUuid(SOME_EXISTING_ADOPTION_ASSET_TX_UUID)) + .thenReturn(Optional.of(SOME_EXISTING_ADOPTION_ASSET_TX_ENTITY)); when(coopAssetsTransactionRepo.save(any(HsOfficeCoopAssetsTransactionEntity.class))) .thenAnswer(invocation -> { final var entity = (HsOfficeCoopAssetsTransactionEntity) invocation.getArgument(0); @@ -358,10 +935,10 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { } private int partnerNumberOf(final String memberNumber) { - return Integer.parseInt(memberNumber.substring("M-".length(), memberNumber.length()-2)); + return Integer.parseInt(memberNumber.substring("M-".length(), memberNumber.length() - 2)); } private String suffixOf(final String memberNumber) { - return memberNumber.substring("M-".length()+5); + return memberNumber.substring("M-".length() + 5); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java index dec419f0..eecd0a4f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java @@ -16,7 +16,8 @@ import net.hostsharing.hsadminng.hs.office.scenarios.membership.CancelMembership import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateMembership; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDepositTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDisbursalTransaction; -import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsRevertTransaction; +import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsRevertSimpleTransaction; +import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsRevertTransferTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsTransferTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesCancellationTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesRevertTransaction; @@ -30,7 +31,6 @@ import net.hostsharing.hsadminng.hs.office.scenarios.subscription.RemoveOperatio import net.hostsharing.hsadminng.hs.office.scenarios.subscription.SubscribeToMailinglist; import net.hostsharing.hsadminng.hs.office.scenarios.subscription.UnsubscribeFromMailinglist; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; -import net.hostsharing.hsadminng.test.IgnoreOnFailure; import net.hostsharing.hsadminng.test.IgnoreOnFailureExtension; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; @@ -395,7 +395,7 @@ class HsOfficeScenarioTests extends ScenarioTest { @Order(4302) @Requires("Membership: M-3101000 - Test AG") void shouldRevertCoopAssetsSubscription() { - new CreateCoopAssetsRevertTransaction(this) + new CreateCoopAssetsRevertSimpleTransaction(this) .given("memberNumber", "M-3101000") .given("comment", "reverting some incorrect transaction") .given("dateOfIncorrectTransaction", "2024-02-15") @@ -419,13 +419,13 @@ class HsOfficeScenarioTests extends ScenarioTest { @Test @Order(4304) @Requires("Coop-Assets M-3101000 - Test AG - DEPOSIT Transaction") - @Produces("Coop-Assets M-3101000 - Test AG - TRANSFER Transaction") + @Produces(explicitly = "Coop-Assets M-3101000 - Test AG - TRANSFER Transaction", implicitly = "Membership M-4303000") void shouldTransferCoopAssets() { new CreateCoopAssetsTransferTransaction(this) .given("transferringMemberNumber", "M-3101000") .given("adoptingMemberNumber", "M-4303000") .given("reference", "transfer 2024-12-31") - .given("valueToDisburse", 2 * 64) + .given("valueToTransfer", 2 * 64) .given("comment", "transfer assets from M-3101000 to M-4303000") .given("transactionDate", "2024-12-31") .doRun(); @@ -433,11 +433,12 @@ class HsOfficeScenarioTests extends ScenarioTest { @Test @Order(4305) - @Requires("Coop-Assets M-3101000 - Test AG - TRANSFER Transaction") - @IgnoreOnFailure("TODO.impl: reverting transfers is not implemented yet") - void shouldRevertCoopAssetsTransfer() { - new CreateCoopAssetsRevertTransaction(this) - .given("memberNumber", "M-3101000") + @Requires("Membership M-4303000") + void shouldRevertCoopAssetsTransferIncludingRelatedAssetAdoption() { + new CreateCoopAssetsRevertTransferTransaction(this) + .given("transferringMemberNumber", "M-3101000") + .given("adoptingMemberNumber", "M-4303000") + .given("transferredValue", 2*64) .given("comment", "reverting some incorrect transfer transaction") .given("dateOfIncorrectTransaction", "2024-02-15") .doRun(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/JsonOptional.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/JsonOptional.java new file mode 100644 index 00000000..2a169fe2 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/JsonOptional.java @@ -0,0 +1,44 @@ +package net.hostsharing.hsadminng.hs.office.scenarios; + + +public final class JsonOptional { + + private final boolean jsonValueGiven; + private final V jsonValue; + + private JsonOptional() { + this.jsonValueGiven = false; + this.jsonValue = null; + } + + private JsonOptional(final V jsonValue) { + this.jsonValueGiven = true; + this.jsonValue = jsonValue; + } + + public static JsonOptional ofValue(final T value) { + return new JsonOptional<>(value); + } + + public static JsonOptional notGiven() { + return new JsonOptional<>(); + } + + public V given() { + if (!jsonValueGiven) { + throw new IllegalStateException("JSON value was not given"); + } + return jsonValue; + } + + public String givenAsString() { + if (jsonValue instanceof Double doubleValue) { + if (doubleValue % 1 == 0) { + return String.valueOf(doubleValue.intValue()); // avoid trailing ".0" + } else { + return doubleValue.toString(); + } + } + return jsonValue == null ? null : jsonValue.toString(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java index 6d3dc2a2..e7aac280 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/PathAssertion.java @@ -19,7 +19,7 @@ public class PathAssertion { public Consumer contains(final String resolvableValue) { return response -> { try { - response.path(path).map(this::asString).contains(ScenarioTest.resolve(resolvableValue, DROP_COMMENTS)); + response.path(path).isEqualTo(ScenarioTest.resolve(resolvableValue, DROP_COMMENTS)); } catch (final AssertionError e) { // without this, the error message is often lacking important context fail(e.getMessage() + " in `path(\"" + path + "\").contains(\"" + resolvableValue + "\")`" ); @@ -37,15 +37,4 @@ public class PathAssertion { } }; } - - private String asString(final Object value) { - if (value instanceof Double doubleValue) { - if (doubleValue % 1 == 0) { - return String.valueOf(doubleValue.intValue()); // avoid trailing ".0" - } else { - return doubleValue.toString(); - } - } - return value.toString(); - } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java index 3e0dab34..4e3557eb 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/ScenarioTest.java @@ -16,6 +16,7 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.testcontainers.shaded.org.apache.commons.lang3.ObjectUtils; import java.lang.reflect.Method; +import java.math.BigDecimal; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -160,6 +161,10 @@ public abstract class ScenarioTest extends ContextBasedTest { properties.put(name, (value instanceof String string) ? resolveTyped(string) : value); } + static void removeProperty(final String propName) { + properties.remove(propName); + } + static Map knowVariables() { final var map = new LinkedHashMap(); ScenarioTest.aliases.forEach((key, value) -> map.put(key, value.uuid())); @@ -172,8 +177,8 @@ public abstract class ScenarioTest extends ContextBasedTest { return resolved; } - public static Object resolveTyped(final String text) { - final var resolved = resolve(text, DROP_COMMENTS); + public static Object resolveTyped(final String resolvableText) { + final var resolved = resolve(resolvableText, DROP_COMMENTS); try { return UUID.fromString(resolved); } catch (final IllegalArgumentException e) { @@ -182,4 +187,14 @@ public abstract class ScenarioTest extends ContextBasedTest { return resolved; } + public static T resolveTyped(final String resolvableText, final Class valueType) { + final var resolvedValue = resolve(resolvableText, DROP_COMMENTS); + if (valueType == BigDecimal.class) { + //noinspection unchecked + return (T) new BigDecimal(resolvedValue); + } + //noinspection unchecked + return (T) resolvedValue; + } + } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java index ec6e0fd6..f0b47bcc 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/TestReport.java @@ -12,9 +12,12 @@ import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Method; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; import java.util.Map; +import java.util.regex.Pattern; +import static java.lang.String.join; import static org.assertj.core.api.Assertions.assertThat; public class TestReport { @@ -41,9 +44,12 @@ public class TestReport { } public void createTestLogMarkdownFile(final TestInfo testInfo) throws IOException { - final var testMethodName = testInfo.getTestMethod().map(Method::getName).orElseThrow(); + final var testMethodName = testInfo.getTestMethod().map(Method::getName) + .map(TestReport::chopShouldPrefix) + .map(TestReport::splitMixedCaseIntoSeparateWords) + .orElseThrow(); final var testMethodOrder = testInfo.getTestMethod().map(m -> m.getAnnotation(Order.class).value()).orElseThrow(); - markdownReportFile = new File(BUILD_DOC_SCENARIOS, testMethodOrder + "-" + testMethodName + ".md"); + markdownReportFile = new File(BUILD_DOC_SCENARIOS, testMethodOrder + ": " + testMethodName + ".md"); markdownReport = new PrintWriter(new FileWriter(markdownReportFile)); print("## Scenario #" + determineScenarioTitle(testInfo)); } @@ -119,6 +125,20 @@ public class TestReport { return result.toString(); } + private static String chopShouldPrefix(final String text) { + return text.replaceAll("^should", ""); + } + + private static String splitMixedCaseIntoSeparateWords(final String text) { + final var WORD_FINDER = Pattern.compile("(([A-Z]?[a-z]+)|([A-Z]))"); + final var matcher = WORD_FINDER.matcher(text); + final var words = new ArrayList(); + while (matcher.find()) { + words.add(matcher.group(0)); + } + return join(" ", words); + } + @SneakyThrows private String currentGitBranch() { try { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java index 27e38e13..58d76759 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/UseCase.java @@ -9,13 +9,14 @@ import lombok.Getter; import lombok.SneakyThrows; import net.hostsharing.hsadminng.reflection.AnnotationFinder; import org.apache.commons.collections4.map.LinkedMap; -import org.assertj.core.api.OptionalAssert; +import org.assertj.core.api.AbstractStringAssert; import org.hibernate.AssertionFailure; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import jakarta.validation.constraints.NotNull; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -27,13 +28,13 @@ import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.UUID; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import static java.net.URLEncoder.encode; +import static java.util.stream.Collectors.joining; import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.KEEP_COMMENTS; import static net.hostsharing.hsadminng.test.DebuggerDetection.isDebuggerAttached; @@ -95,6 +96,9 @@ public abstract class UseCase> { ); final var response = run(); verify(response); + + resetProperties(); + return response; } @@ -109,7 +113,7 @@ public abstract class UseCase> { } public final UseCase given(final String propName, final Object propValue) { - givenProperties.put(propName, propValue); + givenProperties.put(propName, ScenarioTest.resolve(propValue == null ? null : propValue.toString(), KEEP_COMMENTS)); ScenarioTest.putProperty(propName, propValue); return this; } @@ -206,7 +210,8 @@ public abstract class UseCase> { return new PathAssertion(path); } - protected void verify( + @SafeVarargs + protected final void verify( final String title, final Supplier http, final Consumer... assertions) { @@ -236,12 +241,17 @@ public abstract class UseCase> { String resolvePlaceholders() { return ScenarioTest.resolve(template, DROP_COMMENTS); } + } private static Duration seconds(final int secondsIfNoDebuggerAttached) { return isDebuggerAttached() ? Duration.ofHours(1) : Duration.ofSeconds(secondsIfNoDebuggerAttached); } + private void resetProperties() { + givenProperties.forEach((propName, val) -> ScenarioTest.removeProperty(propName)); + } + public final class HttpResponse { @Getter @@ -319,22 +329,25 @@ public abstract class UseCase> { } @SneakyThrows - public String getFromBody(final String path) { - return JsonPath.parse(response.body()).read(ScenarioTest.resolve(path, DROP_COMMENTS)); + public V getFromBody(final String path) { + final var body = response.body(); + final var resolvedPath = ScenarioTest.resolve(path, DROP_COMMENTS); + return JsonPath.parse(body).read(resolvedPath); } + @NotNull @SneakyThrows - public Optional getFromBodyAsOptional(final String path) { + public JsonOptional getFromBodyAsOptional(final String path) { try { - return Optional.ofNullable(JsonPath.parse(response.body()).read(ScenarioTest.resolve(path, DROP_COMMENTS))); + return JsonOptional.ofValue(getFromBody(path)); } catch (final PathNotFoundException e) { - return null; // means the property did not exist at all, not that it was there with value null + return JsonOptional.notGiven(); } } @SneakyThrows - public OptionalAssert path(final String path) { - return assertThat(getFromBodyAsOptional(path)); + public AbstractStringAssert path(final String path) { + return assertThat(getFromBodyAsOptional(path).givenAsString()); } @SneakyThrows @@ -396,4 +409,12 @@ public abstract class UseCase> { private String title(String resultAlias) { return getClass().getSimpleName().replaceAll("([a-z])([A-Z]+)", "$1 $2") + " => " + resultAlias; } + + @Override + public String toString() { + final var properties = givenProperties.entrySet().stream() + .map(e -> "\t" + e.getKey() + "=" + e.getValue()) + .collect(joining("\n")); + return getClass().getSimpleName() + "(\n\t" + properties + "\n)"; + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertSimpleTransaction.java similarity index 71% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransaction.java rename to src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertSimpleTransaction.java index 60d85fbe..0ba384a1 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertSimpleTransaction.java @@ -2,15 +2,15 @@ package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; -public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransaction { +public class CreateCoopAssetsRevertSimpleTransaction extends CreateCoopAssetsTransaction { - public CreateCoopAssetsRevertTransaction(final ScenarioTest testSuite) { + public CreateCoopAssetsRevertSimpleTransaction(final ScenarioTest testSuite) { super(testSuite); requires("CoopAssets-Transaction with incorrect assetValue", alias -> new CreateCoopAssetsDepositTransaction(testSuite) .given("memberNumber", "%{memberNumber}") - .given("reference", "sign %{dateOfIncorrectTransaction}") // same as relatedAssetTx + .given("reference", "sign %{dateOfIncorrectTransaction}") // same text as relatedAssetTx .given("assetValue", 10) .given("comment", "coop-assets deposit transaction with wrong asset value") .given("transactionDate", "%{dateOfIncorrectTransaction}") @@ -21,7 +21,9 @@ public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransacti protected HttpResponse run() { given("transactionType", "REVERSAL"); given("assetValue", -10); + given("reference", "sign %{dateOfIncorrectTransaction}"); // same text as relatedAssetTx given("revertedAssetTx", uuid("CoopAssets-Transaction with incorrect assetValue")); + given("transactionDate", "%{dateOfIncorrectTransaction}"); return super.run(); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransferTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransferTransaction.java new file mode 100644 index 00000000..56ca670c --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransferTransaction.java @@ -0,0 +1,59 @@ +package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; + +import io.restassured.http.ContentType; +import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; +import org.springframework.http.HttpStatus; + +import java.math.BigDecimal; + +import static net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest.resolveTyped; + +public class CreateCoopAssetsRevertTransferTransaction extends CreateCoopAssetsTransaction { + + public CreateCoopAssetsRevertTransferTransaction(final ScenarioTest testSuite) { + super(testSuite); + + requires("Accidental CoopAssets-TRANSFER-Transaction", alias -> + new CreateCoopAssetsTransferTransaction(testSuite) + .given("reference", "transfer %{dateOfIncorrectTransaction}") + .given("valueToTransfer", "%{transferredValue}") + .given("comment", "accidental transfer of assets from %{transferringMemberNumber} to %{adoptingMemberNumber}") + .given("transactionDate", "%{dateOfIncorrectTransaction}") + ); + } + + @Override + protected HttpResponse run() { + given("transactionType", "REVERSAL"); + given("assetValue", "%{transferredValue}"); + given("reference", "sign %{dateOfIncorrectTransaction}"); // same text as relatedAssetTx + given("revertedAssetTx", uuid("Accidental CoopAssets-TRANSFER-Transaction")); + given("transactionDate", "%{dateOfIncorrectTransaction}"); + return super.run(); + } + + @Override + protected void verify(final UseCase.HttpResponse response) { + super.verify(response); + + final var revertedAssetTxUuid = response.getFromBody("revertedAssetTx.uuid"); + given("negativeAssetValue", resolveTyped("%{transferredValue}", BigDecimal.class).negate()); + + verify("Verify Reverted Coop-Assets TRANSFER-Transaction", + () -> httpGet("/api/hs/office/coopassetstransactions/" + revertedAssetTxUuid) + .expecting(HttpStatus.OK).expecting(ContentType.JSON), + path("assetValue").contains("%{negativeAssetValue}"), + path("comment").contains("%{comment}"), + path("valueDate").contains("%{transactionDate}") + ); + + final var adoptionAssetTxUuid = response.getFromBody("revertedAssetTx.['adoptionAssetTx.uuid']"); + + verify("Verify Related Coop-Assets ADOPTION-Transaction Also Got Reverted", + () -> httpGet("/api/hs/office/coopassetstransactions/" + adoptionAssetTxUuid) + .expecting(HttpStatus.OK).expecting(ContentType.JSON), + path("reversalAssetTx.['transferAssetTx.uuid']").contains(revertedAssetTxUuid.toString()) + ); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransferTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransferTransaction.java index bceecfd8..9cfe871f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransferTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransferTransaction.java @@ -20,7 +20,7 @@ public class CreateCoopAssetsTransferTransaction extends CreateCoopAssetsTransac ); requires("Membership: New AG", alias -> new CreateMembership(testSuite) - .given("partnerNumber", toPartnerNumber("%{adoptingMemberNumber}")) + .given("memberNumber", toPartnerNumber("%{adoptingMemberNumber}")) .given("partnerName", "New AG") .given("validFrom", "2024-11-15") .given("newStatus", "ACTIVE") @@ -34,8 +34,7 @@ public class CreateCoopAssetsTransferTransaction extends CreateCoopAssetsTransac given("memberNumber", "%{transferringMemberNumber}"); given("transactionType", "TRANSFER"); - given("assetValue", "-%{valueToDisburse}"); - given("assetValue", "-%{valueToDisburse}"); + given("assetValue", "-%{valueToTransfer}"); return super.run(); } diff --git a/src/test/java/net/hostsharing/hsadminng/lambda/WithNonNullUnitTest.java b/src/test/java/net/hostsharing/hsadminng/lambda/WithNonNullUnitTest.java new file mode 100644 index 00000000..cb947144 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/lambda/WithNonNullUnitTest.java @@ -0,0 +1,30 @@ +package net.hostsharing.hsadminng.lambda; + +import org.junit.jupiter.api.Test; + +import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull; +import static org.assertj.core.api.Assertions.assertThat; + +class WithNonNullUnitTest { + + Boolean didRun = null; + + @Test + void withNonNullRunsBodyIfNotNull() { + didRun = false; + withNonNull("test", nonNullValue -> { + assertThat(nonNullValue).isEqualTo("test"); + didRun = true; + } ); + assertThat(didRun).isTrue(); + } + + @Test + void withNonNullDoesNotRunBodyIfNull() { + didRun = false; + withNonNull(null, nonNullValue -> { + didRun = true; + } ); + assertThat(didRun).isFalse(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonBuilder.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonBuilder.java index 35a29d90..482e20aa 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonBuilder.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonBuilder.java @@ -27,7 +27,7 @@ public class JsonBuilder { * @param value JSON value * @return this JsonBuilder */ - public JsonBuilder with(final String key, final String value) { + public JsonBuilder with(final String key, final Object value) { try { jsonObject.put(key, value); } catch (JSONException e) { diff --git a/src/test/java/net/hostsharing/hsadminng/test/TestUuidGenerator.java b/src/test/java/net/hostsharing/hsadminng/test/TestUuidGenerator.java index b75424c1..e0737bb9 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/TestUuidGenerator.java +++ b/src/test/java/net/hostsharing/hsadminng/test/TestUuidGenerator.java @@ -63,6 +63,9 @@ public class TestUuidGenerator { * @return a constant UUID related to the given index */ public static UUID use(final int index) { + if (staticallyUsedIndexes.contains(index)) { + throw new IllegalArgumentException("index " + index + " already used statically"); + } staticallyUsedIndexes.add(index); return GIVEN_UUIDS.get(index); }