diff --git a/src/main/java/net/hostsharing/hsadminng/config/FormattedBigDecimalDeserializer.java b/src/main/java/net/hostsharing/hsadminng/config/FormattedBigDecimalDeserializer.java new file mode 100644 index 00000000..10991b15 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/config/FormattedBigDecimalDeserializer.java @@ -0,0 +1,28 @@ +package net.hostsharing.hsadminng.config; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import java.io.IOException; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Locale; + +public class FormattedBigDecimalDeserializer extends JsonDeserializer { + + @Override + public BigDecimal deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + final var numberStr = p.getText(); + try { + final var format = NumberFormat.getInstance(Locale.GERMANY); + if (format instanceof DecimalFormat) { + ((DecimalFormat) format).setParseBigDecimal(true); + } + return (BigDecimal) format.parse(numberStr); + } catch (final ParseException e) { + throw new IOException("Failed to parse BigDecimal from string: " + numberStr, e); + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/config/JsonObjectMapperConfiguration.java b/src/main/java/net/hostsharing/hsadminng/config/JsonObjectMapperConfiguration.java index b7011f7f..1afa7ed8 100644 --- a/src/main/java/net/hostsharing/hsadminng/config/JsonObjectMapperConfiguration.java +++ b/src/main/java/net/hostsharing/hsadminng/config/JsonObjectMapperConfiguration.java @@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.config; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.openapitools.jackson.nullable.JsonNullableModule; import org.springframework.context.annotation.Bean; @@ -9,15 +10,25 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import java.math.BigDecimal; + @Configuration public class JsonObjectMapperConfiguration { @Bean @Primary public Jackson2ObjectMapperBuilder customObjectMapper() { + // HOWTO: add JSON converters and specify other JSON mapping configurations return new Jackson2ObjectMapperBuilder() + .modulesToInstall(formattedBigDecimalModule()) .modules(new JsonNullableModule(), new JavaTimeModule()) .featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS, JsonParser.Feature.ALLOW_COMMENTS) .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); } + + private SimpleModule formattedBigDecimalModule() { + final var module = new SimpleModule(); + module.addDeserializer(BigDecimal.class, new FormattedBigDecimalDeserializer()); + return module; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonTypeConverter.java b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonTypeConverter.java index 8c268900..71c04ec0 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonTypeConverter.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonTypeConverter.java @@ -4,6 +4,7 @@ import jakarta.persistence.AttributeConverter; import jakarta.persistence.Converter; import java.util.stream.Stream; +// HOWTO: convert data types for exchange between PostgreSQL and Java/Hibernate/JPA-Entities @Converter(autoApply = true) public class HsOfficePersonTypeConverter implements AttributeConverter { 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 d671aa8e..13af695b 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 @@ -12,10 +12,13 @@ import net.hostsharing.hsadminng.hs.office.scenarios.debitor.FinallyDeleteSepaMa import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DontDeleteDefaultDebitor; import net.hostsharing.hsadminng.hs.office.scenarios.debitor.InvalidateSepaMandateForDebitor; import net.hostsharing.hsadminng.hs.office.scenarios.membership.CancelMembership; -import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateCoopSharesCancellationTransaction; -import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateCoopSharesRevertTransaction; -import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateCoopSharesSubscriptionTransaction; 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.coopshares.CreateCoopSharesCancellationTransaction; +import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesRevertTransaction; +import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesSubscriptionTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.partner.AddOperationsContactToPartner; import net.hostsharing.hsadminng.hs.office.scenarios.partner.CreatePartner; import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DeleteDebitor; @@ -348,7 +351,7 @@ class HsOfficeScenarioTests extends ScenarioTest { void shouldRevertCoopSharesSubscription() { new CreateCoopSharesRevertTransaction(this) .given("memberNumber", "3101000") - .given("comment", "reverting some incorrect subscription") + .given("comment", "reverting some incorrect transaction") .given("dateOfIncorrectTransaction", "2024-02-15") .doRun(); } @@ -367,6 +370,45 @@ class HsOfficeScenarioTests extends ScenarioTest { .doRun(); } + @Test + @Order(4301) + @Requires("Membership: M-3101000 - Test AG") + @Produces("Coop-Assets DEPOSIT Transaction") + void shouldSubscribeCoopAssets() { + new CreateCoopAssetsDepositTransaction(this) + .given("memberNumber", "3101000") + .given("reference", "sign 2024-01-15") + .given("assetValue", 100*64) + .given("comment", "disposal for initial shares") + .given("transactionDate", "2024-01-15") + .doRun(); + } + + @Test + @Order(4302) + @Requires("Membership: M-3101000 - Test AG") + void shouldRevertCoopAssetsSubscription() { + new CreateCoopAssetsRevertTransaction(this) + .given("memberNumber", "3101000") + .given("comment", "reverting some incorrect transaction") + .given("dateOfIncorrectTransaction", "2024-02-15") + .doRun(); + } + + @Test + @Order(4302) + @Requires("Coop-Assets DEPOSIT Transaction") + @Produces("Coop-Assets DISBURSAL Transaction") + void shouldDisburseCoopAssets() { + new CreateCoopAssetsDisbursalTransaction(this) + .given("memberNumber", "3101000") + .given("reference", "cancel 2024-01-15") + .given("valueToDisburse", 8*64) + .given("comment", "disbursal according to shares cancellation") + .given("transactionDate", "2024-02-15") + .doRun(); + } + @Test @Order(4900) @Requires("Membership: M-3101000 - Test AG") 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 6c189db6..6d3dc2a2 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(Object::toString).contains(ScenarioTest.resolve(resolvableValue, DROP_COMMENTS)); + response.path(path).map(this::asString).contains(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,4 +37,15 @@ 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/membership/coopassets/CreateCoopAssetsDepositTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsDepositTransaction.java new file mode 100644 index 00000000..af9b01a1 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsDepositTransaction.java @@ -0,0 +1,12 @@ +package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; + +import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; + +public class CreateCoopAssetsDepositTransaction extends CreateCoopAssetsTransaction { + + public CreateCoopAssetsDepositTransaction(final ScenarioTest testSuite) { + super(testSuite); + + given("transactionType", "DEPOSIT"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsDisbursalTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsDisbursalTransaction.java new file mode 100644 index 00000000..542f75d8 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsDisbursalTransaction.java @@ -0,0 +1,17 @@ +package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; + +import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; + +public class CreateCoopAssetsDisbursalTransaction extends CreateCoopAssetsTransaction { + + public CreateCoopAssetsDisbursalTransaction(final ScenarioTest testSuite) { + super(testSuite); + } + + @Override + protected HttpResponse run() { + given("transactionType", "DISBURSAL"); + given("assetValue", "-%{valueToDisburse}"); + return super.run(); + } +} 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/CreateCoopAssetsRevertTransaction.java new file mode 100644 index 00000000..0750847c --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransaction.java @@ -0,0 +1,27 @@ +package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; + +import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; + +public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransaction { + + public CreateCoopAssetsRevertTransaction(final ScenarioTest testSuite) { + super(testSuite); + + requires("CoopAssets-Transaction with incorrect assetValue", alias -> + new CreateCoopAssetsDepositTransaction(testSuite) + .given("memberNumber", "3101000") + .given("reference", "sign %{dateOfIncorrectTransaction}") // same as revertedAssetTx + .given("assetValue", 10) + .given("comment", "coop-assets deposit transaction with wrong asset value") + .given("transactionDate", "%{dateOfIncorrectTransaction}") + ); + } + + @Override + protected HttpResponse run() { + given("transactionType", "REVERSAL"); + given("assetValue", -100); + given("revertedAssetTx", uuid("CoopAssets-Transaction with incorrect assetValue")); + return super.run(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransaction.java new file mode 100644 index 00000000..56bc2a55 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransaction.java @@ -0,0 +1,53 @@ +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 static io.restassured.http.ContentType.JSON; +import static org.springframework.http.HttpStatus.OK; + +public abstract class CreateCoopAssetsTransaction extends UseCase { + + public CreateCoopAssetsTransaction(final ScenarioTest testSuite) { + super(testSuite); + } + + @Override + protected HttpResponse run() { + + obtain("#{Find }membershipUuid", () -> + httpGet("/api/hs/office/memberships?memberNumber=&{memberNumber}") + .expecting(OK).expecting(JSON).expectArrayElements(1), + response -> response.getFromBody("$[0].uuid") + ); + + return withTitle("Create the Coop-Assets-%{transactionType} Transaction", () -> + httpPost("/api/hs/office/coopassetstransactions", usingJsonBody(""" + { + "membership.uuid": ${membershipUuid}, + "transactionType": ${transactionType}, + "reference": ${reference}, + "assetValue": ${assetValue}, + "comment": ${comment}, + "valueDate": ${transactionDate}, + "revertedAssetTx.uuid": ${revertedAssetTx???} + } + """)) + .expecting(HttpStatus.CREATED).expecting(ContentType.JSON) + ); + } + + @Override + protected void verify(final HttpResponse response) { + verify("Verify Coop-Assets %{transactionType}-Transaction", + () -> httpGet("/api/hs/office/coopassetstransactions/" + response.getLocationUuid()) + .expecting(HttpStatus.OK).expecting(ContentType.JSON), + path("transactionType").contains("%{transactionType}"), + path("assetValue").contains("%{assetValue}"), + path("comment").contains("%{comment}"), + path("valueDate").contains("%{transactionDate}") + ); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateCoopSharesCancellationTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesCancellationTransaction.java similarity index 97% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateCoopSharesCancellationTransaction.java rename to src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesCancellationTransaction.java index 200ae07c..16549b36 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateCoopSharesCancellationTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesCancellationTransaction.java @@ -1,4 +1,4 @@ -package net.hostsharing.hsadminng.hs.office.scenarios.membership; +package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares; import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateCoopSharesRevertTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesRevertTransaction.java similarity index 89% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateCoopSharesRevertTransaction.java rename to src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesRevertTransaction.java index 06ef4101..03963834 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateCoopSharesRevertTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesRevertTransaction.java @@ -1,4 +1,4 @@ -package net.hostsharing.hsadminng.hs.office.scenarios.membership; +package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares; import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; @@ -12,7 +12,7 @@ public class CreateCoopSharesRevertTransaction extends CreateCoopSharesTransacti .given("memberNumber", "3101000") .given("reference", "sign %{dateOfIncorrectTransaction}") // same as revertedShareTx .given("shareCount", 100) - .given("comment", "reverting subscription transaction with wrong share count") + .given("comment", "coop-shares subscription transaction with wrong share count") .given("transactionDate", "%{dateOfIncorrectTransaction}") ); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateCoopSharesSubscriptionTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesSubscriptionTransaction.java similarity index 96% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateCoopSharesSubscriptionTransaction.java rename to src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesSubscriptionTransaction.java index 3aba4f29..84db07dd 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateCoopSharesSubscriptionTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesSubscriptionTransaction.java @@ -1,4 +1,4 @@ -package net.hostsharing.hsadminng.hs.office.scenarios.membership; +package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares; import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateCoopSharesTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesTransaction.java similarity index 95% rename from src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateCoopSharesTransaction.java rename to src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesTransaction.java index 2618b14d..cd8d9c14 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/CreateCoopSharesTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopshares/CreateCoopSharesTransaction.java @@ -1,4 +1,4 @@ -package net.hostsharing.hsadminng.hs.office.scenarios.membership; +package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares; import io.restassured.http.ContentType; import net.hostsharing.hsadminng.hs.office.scenarios.UseCase; @@ -23,7 +23,7 @@ public abstract class CreateCoopSharesTransaction extends UseCase response.getFromBody("$[0].uuid") ); - return withTitle("Create the CoopShares-%{transactionType} Transaction", () -> + return withTitle("Create the Coop-Shares-%{transactionType} Transaction", () -> httpPost("/api/hs/office/coopsharestransactions", usingJsonBody(""" { "membership.uuid": ${membershipUuid},