add advanced scenario-tests for coop-assets (#123)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/123 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
		
							
								
								
									
										18
									
								
								.aliases
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								.aliases
									
									
									
									
									
								
							| @@ -8,12 +8,20 @@ gradleWrapper () { | |||||||
|         return 1 |         return 1 | ||||||
|     fi |     fi | ||||||
|  |  | ||||||
|     TEMPFILE=$(mktemp /tmp/gw.XXXXXX) |     if command -v unbuffer >/dev/null 2>&1; then | ||||||
|     unbuffer ./gradlew "$@" | tee $TEMPFILE |         # if `unbuffer` is available in PATH, use it to print report file-URIs at the end | ||||||
|  |         TEMPFILE=$(mktemp /tmp/gw.XXXXXX) | ||||||
|  |         unbuffer ./gradlew "$@" | tee $TEMPFILE | ||||||
|  |         echo | ||||||
|  |         grep --color=never "Report:" $TEMPFILE | ||||||
|  |         rm $TEMPFILE | ||||||
|  |     else | ||||||
|  |         # if `unbuffer` is not in PATH, simply run gradle | ||||||
|  |         ./gradlew "$@" | ||||||
|  |         echo "HINT: it's suggested to install 'unbuffer' to print report URIs at the end of a gradle run" | ||||||
|  |     fi | ||||||
|  |  | ||||||
|  |  | ||||||
|     echo |  | ||||||
|     grep --color=never "Report:" $TEMPFILE |  | ||||||
|     rm $TEMPFILE |  | ||||||
| } | } | ||||||
|  |  | ||||||
| postgresAutodoc () { | postgresAutodoc () { | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							| @@ -575,7 +575,7 @@ that and creates too many (grant- and role-) rows and too even tables which coul | |||||||
|  |  | ||||||
| The basic idea is always to always have a fixed set of ordered role-types which apply for all DB-tables under RBAC, | The basic idea is always to always have a fixed set of ordered role-types which apply for all DB-tables under RBAC, | ||||||
| e.g. OWNER>ADMIN>AGENT\[>PROXY?\]>TENENT>REFERRER. | e.g. OWNER>ADMIN>AGENT\[>PROXY?\]>TENENT>REFERRER. | ||||||
| Grants between these for the same DB-row would be implicit by order comparision. | Grants between these for the same DB-row would be implicit by order comparison. | ||||||
| This way we would get rid of all explicit grants within the same DB-row | This way we would get rid of all explicit grants within the same DB-row | ||||||
| and would not need the `rbac.role` table anymore. | and would not need the `rbac.role` table anymore. | ||||||
| We would also reduce the depth of the expensive recursive CTE-query. | We would also reduce the depth of the expensive recursive CTE-query. | ||||||
| @@ -591,8 +591,20 @@ E.g. the uuid of the target main object is often taken from an uuid of a sub-sub | |||||||
| (For now, use `StrictMapper` to avoid this, for the case it happens.) | (For now, use `StrictMapper` to avoid this, for the case it happens.) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Too Many Business-Rules Implemented in Controllers | ||||||
|  |  | ||||||
|  | Some REST-Controllers implement too much code for business-roles. | ||||||
|  | This should be extracted to services. | ||||||
|  |  | ||||||
|  |  | ||||||
| ## How To ... | ## How To ... | ||||||
|  |  | ||||||
|  | Besides the following *How Tos* you can also find several *How Tos* in the source code: | ||||||
|  |  | ||||||
|  | ```sh | ||||||
|  | grep -r HOWTO src | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ### How to Configure .pgpass for the Default PostgreSQL Database? | ### How to Configure .pgpass for the Default PostgreSQL Database? | ||||||
|  |  | ||||||
| To access the default database schema as used during development, add this line to your `.pgpass` file in your users home directory: | To access the default database schema as used during development, add this line to your `.pgpass` file in your users home directory: | ||||||
|   | |||||||
| @@ -445,3 +445,8 @@ tasks.register('convertMarkdownToHtml') { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| convertMarkdownToHtml.dependsOn scenarioTests | convertMarkdownToHtml.dependsOn scenarioTests | ||||||
|  |  | ||||||
|  | // shortcut for compiling all files | ||||||
|  | tasks.register('compile') { | ||||||
|  |     dependsOn 'compileJava', 'compileTestJava' | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,10 +1,15 @@ | |||||||
| package net.hostsharing.hsadminng.hs.office.coopassets; | package net.hostsharing.hsadminng.hs.office.coopassets; | ||||||
|  |  | ||||||
| import net.hostsharing.hsadminng.context.Context; | import net.hostsharing.hsadminng.context.Context; | ||||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi; |  | ||||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*; |  | ||||||
| import net.hostsharing.hsadminng.errors.MultiValidationException; | import net.hostsharing.hsadminng.errors.MultiValidationException; | ||||||
| import net.hostsharing.hsadminng.mapper.StandardMapper; | import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi; | ||||||
|  | import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionInsertResource; | ||||||
|  | import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionResource; | ||||||
|  | import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource; | ||||||
|  | import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; | ||||||
|  | import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository; | ||||||
|  | import net.hostsharing.hsadminng.mapper.StrictMapper; | ||||||
|  | import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; | ||||||
| import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||||
| import org.springframework.format.annotation.DateTimeFormat; | import org.springframework.format.annotation.DateTimeFormat; | ||||||
| import org.springframework.format.annotation.DateTimeFormat.ISO; | import org.springframework.format.annotation.DateTimeFormat.ISO; | ||||||
| @@ -14,13 +19,19 @@ import org.springframework.web.bind.annotation.RestController; | |||||||
| import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; | import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; | ||||||
|  |  | ||||||
| import jakarta.persistence.EntityNotFoundException; | import jakarta.persistence.EntityNotFoundException; | ||||||
|  | import jakarta.validation.ValidationException; | ||||||
| import java.time.LocalDate; | import java.time.LocalDate; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
| import java.util.function.BiConsumer; | import java.util.function.BiConsumer; | ||||||
|  |  | ||||||
| import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.*; | import static java.util.Optional.ofNullable; | ||||||
|  | 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; | ||||||
|  |  | ||||||
| @RestController | @RestController | ||||||
| public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi { | public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi { | ||||||
| @@ -29,11 +40,17 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse | |||||||
|     private Context context; |     private Context context; | ||||||
|  |  | ||||||
|     @Autowired |     @Autowired | ||||||
|     private StandardMapper mapper; |     private StrictMapper mapper; | ||||||
|  |  | ||||||
|  |     @Autowired | ||||||
|  |     private EntityManagerWrapper emw; | ||||||
|  |  | ||||||
|     @Autowired |     @Autowired | ||||||
|     private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo; |     private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo; | ||||||
|  |  | ||||||
|  |     @Autowired | ||||||
|  |     private HsOfficeMembershipRepository membershipRepo; | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     @Transactional(readOnly = true) |     @Transactional(readOnly = true) | ||||||
|     public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> getListOfCoopAssets( |     public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> getListOfCoopAssets( | ||||||
| @@ -49,7 +66,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse | |||||||
|                 fromValueDate, |                 fromValueDate, | ||||||
|                 toValueDate); |                 toValueDate); | ||||||
|  |  | ||||||
|         final var resources = mapper.mapList(entities, HsOfficeCoopAssetsTransactionResource.class); |         final var resources = mapper.mapList(entities, HsOfficeCoopAssetsTransactionResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); | ||||||
|         return ResponseEntity.ok(resources); |         return ResponseEntity.ok(resources); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -63,7 +80,10 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse | |||||||
|         context.define(currentSubject, assumedRoles); |         context.define(currentSubject, assumedRoles); | ||||||
|         validate(requestBody); |         validate(requestBody); | ||||||
|  |  | ||||||
|         final var entityToSave = mapper.map(requestBody, HsOfficeCoopAssetsTransactionEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); |         final var entityToSave = mapper.map( | ||||||
|  |                 requestBody, | ||||||
|  |                 HsOfficeCoopAssetsTransactionEntity.class, | ||||||
|  |                 RESOURCE_TO_ENTITY_POSTMAPPER); | ||||||
|         final var saved = coopAssetsTransactionRepo.save(entityToSave); |         final var saved = coopAssetsTransactionRepo.save(entityToSave); | ||||||
|  |  | ||||||
|         final var uri = |         final var uri = | ||||||
| @@ -71,14 +91,14 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse | |||||||
|                         .path("/api/hs/office/coopassetstransactions/{id}") |                         .path("/api/hs/office/coopassetstransactions/{id}") | ||||||
|                         .buildAndExpand(saved.getUuid()) |                         .buildAndExpand(saved.getUuid()) | ||||||
|                         .toUri(); |                         .toUri(); | ||||||
|         final var mapped = mapper.map(saved, HsOfficeCoopAssetsTransactionResource.class); |         final var mapped = mapper.map(saved, HsOfficeCoopAssetsTransactionResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); | ||||||
|         return ResponseEntity.created(uri).body(mapped); |         return ResponseEntity.created(uri).body(mapped); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     @Transactional(readOnly = true) |     @Transactional(readOnly = true) | ||||||
|     public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getSingleCoopAssetTransactionByUuid( |     public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getSingleCoopAssetTransactionByUuid( | ||||||
|         final String currentSubject, final String assumedRoles, final UUID assetTransactionUuid) { |             final String currentSubject, final String assumedRoles, final UUID assetTransactionUuid) { | ||||||
|  |  | ||||||
|         context.define(currentSubject, assumedRoles); |         context.define(currentSubject, assumedRoles); | ||||||
|  |  | ||||||
| @@ -101,7 +121,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse | |||||||
|     private static void validateDebitTransaction( |     private static void validateDebitTransaction( | ||||||
|             final HsOfficeCoopAssetsTransactionInsertResource requestBody, |             final HsOfficeCoopAssetsTransactionInsertResource requestBody, | ||||||
|             final ArrayList<String> violations) { |             final ArrayList<String> violations) { | ||||||
|         if (List.of(DEPOSIT, ADOPTION).contains(requestBody.getTransactionType()) |         if (List.of(DEPOSIT, HsOfficeCoopAssetsTransactionTypeResource.ADOPTION).contains(requestBody.getTransactionType()) | ||||||
|                 && requestBody.getAssetValue().signum() < 0) { |                 && requestBody.getAssetValue().signum() < 0) { | ||||||
|             violations.add("for %s, assetValue must be positive but is \"%.2f\"".formatted( |             violations.add("for %s, assetValue must be positive but is \"%.2f\"".formatted( | ||||||
|                     requestBody.getTransactionType(), requestBody.getAssetValue())); |                     requestBody.getTransactionType(), requestBody.getAssetValue())); | ||||||
| @@ -127,10 +147,111 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     final BiConsumer<HsOfficeCoopAssetsTransactionInsertResource, HsOfficeCoopAssetsTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { |     final BiConsumer<HsOfficeCoopAssetsTransactionEntity, HsOfficeCoopAssetsTransactionResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { | ||||||
|         if ( resource.getRevertedAssetTxUuid() != null ) { |         resource.setMembershipUuid(entity.getMembership().getUuid()); | ||||||
|             entity.setRevertedAssetTx(coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid()) |         resource.setMembershipMemberNumber(entity.getMembership().getTaggedMemberNumber()); | ||||||
|                     .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] reverseEntityUuid %s not found".formatted(resource.getRevertedAssetTxUuid())))); |  | ||||||
|  |         if (entity.getReversalAssetTx() != null) { | ||||||
|  |             resource.getReversalAssetTx().setRevertedAssetTxUuid(entity.getUuid()); | ||||||
|  |             resource.getReversalAssetTx().setMembershipUuid(entity.getMembership().getUuid()); | ||||||
|  |             resource.getReversalAssetTx().setMembershipMemberNumber(entity.getTaggedMemberNumber()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (entity.getRevertedAssetTx() != null) { | ||||||
|  |             resource.getRevertedAssetTx().setReversalAssetTxUuid(entity.getUuid()); | ||||||
|  |             resource.getRevertedAssetTx().setMembershipUuid(entity.getMembership().getUuid()); | ||||||
|  |             resource.getRevertedAssetTx().setMembershipMemberNumber(entity.getTaggedMemberNumber()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (entity.getAdoptionAssetTx() != null) { | ||||||
|  |             resource.getAdoptionAssetTx().setTransferAssetTxUuid(entity.getUuid()); | ||||||
|  |             resource.getAdoptionAssetTx().setMembershipUuid(entity.getAdoptionAssetTx().getMembership().getUuid()); | ||||||
|  |             resource.getAdoptionAssetTx().setMembershipMemberNumber(entity.getAdoptionAssetTx().getTaggedMemberNumber()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (entity.getTransferAssetTx() != null) { | ||||||
|  |             resource.getTransferAssetTx().setAdoptionAssetTxUuid(entity.getUuid()); | ||||||
|  |             resource.getTransferAssetTx().setMembershipUuid(entity.getTransferAssetTx().getMembership().getUuid()); | ||||||
|  |             resource.getTransferAssetTx().setMembershipMemberNumber(entity.getTransferAssetTx().getTaggedMemberNumber()); | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| }; |  | ||||||
|  |     final BiConsumer<HsOfficeCoopAssetsTransactionInsertResource, HsOfficeCoopAssetsTransactionEntity> 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( | ||||||
|  |                             resource.getMembershipUuid()))); | ||||||
|  |             entity.setMembership(membership); | ||||||
|  |         } | ||||||
|  |         if (resource.getRevertedAssetTxUuid() != null) { | ||||||
|  |             final var revertedAssetTx = coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid()) | ||||||
|  |                     .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] revertedEntityUuid %s not found".formatted( | ||||||
|  |                             resource.getRevertedAssetTxUuid()))); | ||||||
|  |             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()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         final var adoptingMembership = determineAdoptingMembership(resource); | ||||||
|  |         if (adoptingMembership != null) { | ||||||
|  |             final var adoptingAssetTx = coopAssetsTransactionRepo.save(createAdoptingAssetTx(entity, adoptingMembership)); | ||||||
|  |             entity.setAdoptionAssetTx(adoptingAssetTx); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     private HsOfficeMembershipEntity determineAdoptingMembership(final HsOfficeCoopAssetsTransactionInsertResource resource) { | ||||||
|  |         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=" | ||||||
|  |                             + resource.getTransactionType()); | ||||||
|  |                 // @formatter:on | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (adoptingMembershipUuid != null) { | ||||||
|  |             final var adoptingMembership = membershipRepo.findByUuid(adoptingMembershipUuid); | ||||||
|  |             return adoptingMembership.orElseThrow(() -> | ||||||
|  |                     new ValidationException( | ||||||
|  |                             "adoptingMembership.uuid='" + adoptingMembershipUuid + "' not found or not accessible")); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (adoptingMembershipMemberNumber != null) { | ||||||
|  |             final var adoptingMemberNumber = Integer.valueOf(adoptingMembershipMemberNumber.substring("M-".length())); | ||||||
|  |             final var adoptingMembership = membershipRepo.findMembershipByMemberNumber(adoptingMemberNumber); | ||||||
|  |             if (adoptingMembership != null) { | ||||||
|  |                 return adoptingMembership; | ||||||
|  |             } | ||||||
|  |             throw new ValidationException("adoptingMembership.memberNumber='" + adoptingMembershipMemberNumber | ||||||
|  |                     + "' 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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private HsOfficeCoopAssetsTransactionEntity createAdoptingAssetTx( | ||||||
|  |             final HsOfficeCoopAssetsTransactionEntity transferAssetTxEntity, | ||||||
|  |             final HsOfficeMembershipEntity adoptingMembership) { | ||||||
|  |         return HsOfficeCoopAssetsTransactionEntity.builder() | ||||||
|  |                 .membership(adoptingMembership) | ||||||
|  |                 .transactionType(HsOfficeCoopAssetsTransactionType.ADOPTION) | ||||||
|  |                 .transferAssetTx(transferAssetTxEntity) | ||||||
|  |                 .assetValue(transferAssetTxEntity.getAssetValue().negate()) | ||||||
|  |                 .comment(transferAssetTxEntity.getComment()) | ||||||
|  |                 .reference(transferAssetTxEntity.getReference()) | ||||||
|  |                 .valueDate(transferAssetTxEntity.getValueDate()) | ||||||
|  |                 .build(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -50,8 +50,10 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE | |||||||
|             .withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue) |             .withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue) | ||||||
|             .withProp(HsOfficeCoopAssetsTransactionEntity::getReference) |             .withProp(HsOfficeCoopAssetsTransactionEntity::getReference) | ||||||
|             .withProp(HsOfficeCoopAssetsTransactionEntity::getComment) |             .withProp(HsOfficeCoopAssetsTransactionEntity::getComment) | ||||||
|             .withProp(at -> ofNullable(at.getRevertedAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null)) |             .withProp(HsOfficeCoopAssetsTransactionEntity::getRevertedAssetTx) | ||||||
|             .withProp(at -> ofNullable(at.getReversalAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null)) |             .withProp(HsOfficeCoopAssetsTransactionEntity::getReversalAssetTx) | ||||||
|  |             .withProp(HsOfficeCoopAssetsTransactionEntity::getAdoptionAssetTx) | ||||||
|  |             .withProp(HsOfficeCoopAssetsTransactionEntity::getTransferAssetTx) | ||||||
|             .quotedValues(false); |             .quotedValues(false); | ||||||
|  |  | ||||||
|     @Id |     @Id | ||||||
| @@ -95,16 +97,24 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE | |||||||
|     @Column(name = "comment") |     @Column(name = "comment") | ||||||
|     private String comment; |     private String comment; | ||||||
|  |  | ||||||
|     /** |     // Optionally, the UUID of the corresponding transaction for a reversal transaction. | ||||||
|      * Optionally, the UUID of the corresponding transaction for an reversal transaction. |     @OneToOne(cascade = CascadeType.PERSIST) // TODO.impl: can probably be removed after office data migration | ||||||
|      */ |  | ||||||
|     @OneToOne |  | ||||||
|     @JoinColumn(name = "revertedassettxuuid") |     @JoinColumn(name = "revertedassettxuuid") | ||||||
|     private HsOfficeCoopAssetsTransactionEntity revertedAssetTx; |     private HsOfficeCoopAssetsTransactionEntity revertedAssetTx; | ||||||
|  |  | ||||||
|  |     // and the other way around | ||||||
|     @OneToOne(mappedBy = "revertedAssetTx") |     @OneToOne(mappedBy = "revertedAssetTx") | ||||||
|     private HsOfficeCoopAssetsTransactionEntity reversalAssetTx; |     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 | ||||||
|  |     @JoinColumn(name = "assetadoptiontxuuid") | ||||||
|  |     private HsOfficeCoopAssetsTransactionEntity adoptionAssetTx; | ||||||
|  |  | ||||||
|  |     // and the other way around | ||||||
|  |     @OneToOne(mappedBy = "adoptionAssetTx") | ||||||
|  |     private HsOfficeCoopAssetsTransactionEntity transferAssetTx; | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public HsOfficeCoopAssetsTransactionEntity load() { |     public HsOfficeCoopAssetsTransactionEntity load() { | ||||||
|         BaseEntity.super.load(); |         BaseEntity.super.load(); | ||||||
|   | |||||||
| @@ -80,12 +80,6 @@ public final class Stringify<B> { | |||||||
|                 .map(prop -> PropertyValue.of(prop, prop.getter.apply(object))) |                 .map(prop -> PropertyValue.of(prop, prop.getter.apply(object))) | ||||||
|                 .filter(Objects::nonNull) |                 .filter(Objects::nonNull) | ||||||
|                 .filter(PropertyValue::nonEmpty) |                 .filter(PropertyValue::nonEmpty) | ||||||
|                 .map(propVal -> { |  | ||||||
|                     if (propVal.rawValue instanceof Stringifyable stringifyable) { |  | ||||||
|                         return new PropertyValue<>(propVal.prop, propVal.rawValue, stringifyable.toShortString()); |  | ||||||
|                     } |  | ||||||
|                     return propVal; |  | ||||||
|                 }) |  | ||||||
|                 .map(propVal -> propName(propVal, "=") + optionallyQuoted(propVal)) |                 .map(propVal -> propName(propVal, "=") + optionallyQuoted(propVal)) | ||||||
|                 .collect(Collectors.joining(separator)); |                 .collect(Collectors.joining(separator)); | ||||||
|         return idProp != null |         return idProp != null | ||||||
| @@ -131,7 +125,11 @@ public final class Stringify<B> { | |||||||
|     private record PropertyValue<B>(Property<B> prop, Object rawValue, String value) { |     private record PropertyValue<B>(Property<B> prop, Object rawValue, String value) { | ||||||
|  |  | ||||||
|         static <B> PropertyValue<B> of(Property<B> prop, Object rawValue) { |         static <B> PropertyValue<B> of(Property<B> prop, Object rawValue) { | ||||||
|             return rawValue != null ? new PropertyValue<>(prop, rawValue, rawValue.toString()) : null; |             return rawValue != null ? new PropertyValue<>(prop, rawValue, toStringOrShortString(rawValue)) : null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private static String toStringOrShortString(final Object rawValue) { | ||||||
|  |             return rawValue instanceof Stringifyable stringifyable ? stringifyable.toShortString() : rawValue.toString(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         boolean nonEmpty() { |         boolean nonEmpty() { | ||||||
|   | |||||||
| @@ -20,6 +20,15 @@ components: | |||||||
|                 uuid: |                 uuid: | ||||||
|                     type: string |                     type: string | ||||||
|                     format: uuid |                     format: uuid | ||||||
|  |                 membership.uuid: | ||||||
|  |                     type: string | ||||||
|  |                     format: uuid | ||||||
|  |                     nullable: false | ||||||
|  |                 membership.memberNumber: | ||||||
|  |                     type: string | ||||||
|  |                     minLength: 9 | ||||||
|  |                     maxLength: 9 | ||||||
|  |                     pattern: 'M-[0-9]{7}' | ||||||
|                 transactionType: |                 transactionType: | ||||||
|                     $ref: '#/components/schemas/HsOfficeCoopAssetsTransactionType' |                     $ref: '#/components/schemas/HsOfficeCoopAssetsTransactionType' | ||||||
|                 assetValue: |                 assetValue: | ||||||
| @@ -32,20 +41,36 @@ components: | |||||||
|                     type: string |                     type: string | ||||||
|                 comment: |                 comment: | ||||||
|                     type: string |                     type: string | ||||||
|  |                 adoptionAssetTx: | ||||||
|  |                     # a TRANSFER tx must refer to the related ADOPTION tx | ||||||
|  |                     $ref: '#/components/schemas/HsOfficeRelatedCoopAssetsTransaction' | ||||||
|  |                 transferAssetTx: | ||||||
|  |                     # an ADOPTION tx must refer to the related TRANSFER tx | ||||||
|  |                     $ref: '#/components/schemas/HsOfficeRelatedCoopAssetsTransaction' | ||||||
|                 revertedAssetTx: |                 revertedAssetTx: | ||||||
|                     $ref: '#/components/schemas/HsOfficeReferencedCoopAssetsTransaction' |                     # a REVERSAL tx must refer to the related tx, which can be of any type but REVERSAL | ||||||
|  |                     $ref: '#/components/schemas/HsOfficeRelatedCoopAssetsTransaction' | ||||||
|                 reversalAssetTx: |                 reversalAssetTx: | ||||||
|                     $ref: '#/components/schemas/HsOfficeReferencedCoopAssetsTransaction' |                     # a reverted tx, which can be any but REVERSAL, must refer to the related REVERSAL tx | ||||||
|  |                     $ref: '#/components/schemas/HsOfficeRelatedCoopAssetsTransaction' | ||||||
|  |  | ||||||
|         HsOfficeReferencedCoopAssetsTransaction: |         HsOfficeRelatedCoopAssetsTransaction: | ||||||
|             description: |             description: | ||||||
|                 Similar to `HsOfficeCoopAssetsTransaction` but without the self-referencing properties  |                 Similar to `HsOfficeCoopAssetsTransaction` but just the UUID of the related property, to avoid recursive JSON. | ||||||
|                 (`revertedAssetTx` and `reversalAssetTx`), to avoid recursive JSON. |  | ||||||
|             type: object |             type: object | ||||||
|             properties: |             properties: | ||||||
|                 uuid: |                 uuid: | ||||||
|                     type: string |                     type: string | ||||||
|                     format: uuid |                     format: uuid | ||||||
|  |                 membership.uuid: | ||||||
|  |                     type: string | ||||||
|  |                     format: uuid | ||||||
|  |                     nullable: false | ||||||
|  |                 membership.memberNumber: | ||||||
|  |                     type: string | ||||||
|  |                     minLength: 9 | ||||||
|  |                     maxLength: 9 | ||||||
|  |                     pattern: 'M-[0-9]{7}' | ||||||
|                 transactionType: |                 transactionType: | ||||||
|                     $ref: '#/components/schemas/HsOfficeCoopAssetsTransactionType' |                     $ref: '#/components/schemas/HsOfficeCoopAssetsTransactionType' | ||||||
|                 assetValue: |                 assetValue: | ||||||
| @@ -58,6 +83,22 @@ components: | |||||||
|                     type: string |                     type: string | ||||||
|                 comment: |                 comment: | ||||||
|                     type: string |                     type: string | ||||||
|  |                 adoptionAssetTx.uuid: | ||||||
|  |                     description: a TRANSFER tx must refer to the related ADOPTION tx | ||||||
|  |                     type: string | ||||||
|  |                     format: uuid | ||||||
|  |                 transferAssetTx.uuid: | ||||||
|  |                     description: an ADOPTION tx must refer to the related TRANSFER tx | ||||||
|  |                     type: string | ||||||
|  |                     format: uuid | ||||||
|  |                 revertedAssetTx.uuid: | ||||||
|  |                     description: a REVERSAL tx must refer to the related tx, which can be of any type but REVERSAL | ||||||
|  |                     type: string | ||||||
|  |                     format: uuid | ||||||
|  |                 reversalAssetTx.uuid: | ||||||
|  |                     description: a reverted tx, which can be any but REVERSAL, must refer to the related REVERSAL tx | ||||||
|  |                     type: string | ||||||
|  |                     format: uuid | ||||||
|  |  | ||||||
|         HsOfficeCoopAssetsTransactionInsert: |         HsOfficeCoopAssetsTransactionInsert: | ||||||
|             type: object |             type: object | ||||||
| @@ -83,6 +124,14 @@ components: | |||||||
|                 revertedAssetTx.uuid: |                 revertedAssetTx.uuid: | ||||||
|                     type: string |                     type: string | ||||||
|                     format: uuid |                     format: uuid | ||||||
|  |                 adoptingMembership.uuid: | ||||||
|  |                     type: string | ||||||
|  |                     format: uuid | ||||||
|  |                 adoptingMembership.memberNumber: | ||||||
|  |                     type: string | ||||||
|  |                     minLength: 9 | ||||||
|  |                     maxLength: 9 | ||||||
|  |                     pattern: 'M-[0-9]{7}' | ||||||
|             required: |             required: | ||||||
|                 - membership.uuid |                 - membership.uuid | ||||||
|                 - transactionType |                 - transactionType | ||||||
|   | |||||||
| @@ -16,6 +16,10 @@ components: | |||||||
|                 uuid: |                 uuid: | ||||||
|                     type: string |                     type: string | ||||||
|                     format: uuid |                     format: uuid | ||||||
|  |                 membership.uuid: | ||||||
|  |                     type: string | ||||||
|  |                     format: uuid | ||||||
|  |                     nullable: false | ||||||
|                 transactionType: |                 transactionType: | ||||||
|                     $ref: '#/components/schemas/HsOfficeCoopSharesTransactionType' |                     $ref: '#/components/schemas/HsOfficeCoopSharesTransactionType' | ||||||
|                 shareCount: |                 shareCount: | ||||||
| @@ -41,6 +45,10 @@ components: | |||||||
|                 uuid: |                 uuid: | ||||||
|                     type: string |                     type: string | ||||||
|                     format: uuid |                     format: uuid | ||||||
|  |                 membership.uuid: | ||||||
|  |                     type: string | ||||||
|  |                     format: uuid | ||||||
|  |                     nullable: false | ||||||
|                 transactionType: |                 transactionType: | ||||||
|                     $ref: '#/components/schemas/HsOfficeCoopSharesTransactionType' |                     $ref: '#/components/schemas/HsOfficeCoopSharesTransactionType' | ||||||
|                 shareCount: |                 shareCount: | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ create table if not exists hs_office.coopassettx | |||||||
|     assetValue          numeric(12,2) not null, -- https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_money |     assetValue          numeric(12,2) not null, -- https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_money | ||||||
|     reference           varchar(48) not null, |     reference           varchar(48) not null, | ||||||
|     revertedAssetTxUuid uuid unique REFERENCES hs_office.coopassettx(uuid) DEFERRABLE INITIALLY DEFERRED, |     revertedAssetTxUuid uuid unique REFERENCES hs_office.coopassettx(uuid) DEFERRABLE INITIALLY DEFERRED, | ||||||
|  |     assetAdoptionTxUuid uuid unique REFERENCES hs_office.coopassettx(uuid) DEFERRABLE INITIALLY DEFERRED, | ||||||
|     comment             varchar(512) |     comment             varchar(512) | ||||||
| ); | ); | ||||||
| --// | --// | ||||||
| @@ -35,9 +36,20 @@ create table if not exists hs_office.coopassettx | |||||||
| -- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||||||
|  |  | ||||||
| alter table hs_office.coopassettx | alter table hs_office.coopassettx | ||||||
|     add constraint reverse_entry_missing |     add constraint reversal_asset_tx_must_have_reverted_asset_tx | ||||||
|         check ( transactionType = 'REVERSAL' and revertedAssetTxUuid is not null |         check (transactionType <> 'REVERSAL' or revertedAssetTxUuid is not null); | ||||||
|              or transactionType <> 'REVERSAL' and revertedAssetTxUuid is null); |  | ||||||
|  | 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'); | ||||||
|  |  | ||||||
|  | alter table hs_office.coopassettx | ||||||
|  |     add constraint transfer_asset_tx_must_have_adopted_asset_tx | ||||||
|  |         check (transactionType <> 'TRANSFER' or assetAdoptionTxUuid is not null); | ||||||
|  |  | ||||||
|  | alter table hs_office.coopassettx | ||||||
|  |     add constraint non_transfer_asset_tx_must_not_have_adopted_asset_tx | ||||||
|  |         check (transactionType = 'TRANSFER' or assetAdoptionTxUuid is null); | ||||||
| --// | --// | ||||||
|  |  | ||||||
| -- ============================================================================ | -- ============================================================================ | ||||||
|   | |||||||
| @@ -15,7 +15,9 @@ create or replace procedure hs_office.coopassettx_create_test_data( | |||||||
|     language plpgsql as $$ |     language plpgsql as $$ | ||||||
| declare | declare | ||||||
|     membership              hs_office.membership; |     membership              hs_office.membership; | ||||||
|     lossEntryUuid           uuid; |     invalidLossTx           uuid; | ||||||
|  |     transferTx              uuid; | ||||||
|  |     adoptionTx              uuid; | ||||||
| begin | begin | ||||||
|     select m.uuid |     select m.uuid | ||||||
|         from hs_office.membership m |         from hs_office.membership m | ||||||
| @@ -25,14 +27,18 @@ begin | |||||||
|         into membership; |         into membership; | ||||||
|  |  | ||||||
|     raise notice 'creating test coopAssetsTransaction: %', givenPartnerNumber || givenMemberNumberSuffix; |     raise notice 'creating test coopAssetsTransaction: %', givenPartnerNumber || givenMemberNumberSuffix; | ||||||
|     lossEntryUuid := uuid_generate_v4(); |     invalidLossTx := uuid_generate_v4(); | ||||||
|  |     transferTx := uuid_generate_v4(); | ||||||
|  |     adoptionTx := uuid_generate_v4(); | ||||||
|     insert |     insert | ||||||
|         into hs_office.coopassettx(uuid, membershipuuid, transactiontype, valuedate, assetvalue, reference, comment, revertedAssetTxUuid) |         into hs_office.coopassettx(uuid, membershipuuid, transactiontype, valuedate, assetvalue, reference, comment, revertedAssetTxUuid, assetAdoptionTxUuid) | ||||||
|         values |         values | ||||||
|             (uuid_generate_v4(),  membership.uuid, 'DEPOSIT',    '2010-03-15',  320.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-1', 'initial deposit', null), |             (uuid_generate_v4(),  membership.uuid, 'DEPOSIT',    '2010-03-15',  320.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-1', 'initial deposit', null, null), | ||||||
|             (uuid_generate_v4(),  membership.uuid, 'DISBURSAL',  '2021-09-01', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-2', 'partial disbursal', null), |             (uuid_generate_v4(),  membership.uuid, 'DISBURSAL',  '2021-09-01', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-2', 'partial disbursal', null, null), | ||||||
|             (lossEntryUuid,       membership.uuid, 'DEPOSIT',    '2022-10-20',  128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some loss', null), |             (invalidLossTx,       membership.uuid, 'DEPOSIT',    '2022-10-20',  128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some loss', null, null), | ||||||
|             (uuid_generate_v4(),  membership.uuid, 'REVERSAL', '2022-10-21', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some reversal', lossEntryUuid); |             (uuid_generate_v4(),  membership.uuid, 'REVERSAL', '2022-10-21', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some reversal', invalidLossTx, null), | ||||||
|  |             (transferTx,          membership.uuid, 'TRANSFER', '2023-12-31', -192.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some reversal', null, adoptionTx), | ||||||
|  |             (adoptionTx,            membership.uuid, 'ADOPTION', '2023-12-31', 192.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some reversal', null, null); | ||||||
| end; $$; | end; $$; | ||||||
| --// | --// | ||||||
|  |  | ||||||
|   | |||||||
| @@ -449,7 +449,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport { | |||||||
|                    1094=CoopAssetsTransaction(M-1000300: 2023-10-06, DEPOSIT, 3072, 1000300, Kapitalerhoehung - Ueberweisung), |                    1094=CoopAssetsTransaction(M-1000300: 2023-10-06, DEPOSIT, 3072, 1000300, Kapitalerhoehung - Ueberweisung), | ||||||
|                    31000=CoopAssetsTransaction(M-1002000: 2000-12-06, DEPOSIT, 128.00, 1002000, for subscription B), |                    31000=CoopAssetsTransaction(M-1002000: 2000-12-06, DEPOSIT, 128.00, 1002000, for subscription B), | ||||||
|                    32000=CoopAssetsTransaction(M-1000300: 2005-01-10, DEPOSIT, 2560.00, 1000300, for subscription C), |                    32000=CoopAssetsTransaction(M-1000300: 2005-01-10, DEPOSIT, 2560.00, 1000300, for subscription C), | ||||||
|                    33001=CoopAssetsTransaction(M-1000300: 2005-01-10, TRANSFER, -512.00, 1000300, for transfer to 10), |                    33001=CoopAssetsTransaction(M-1000300: 2005-01-10, TRANSFER, -512.00, 1000300, for transfer to 10, M-1002000:ADO:+512.00), | ||||||
|                    33002=CoopAssetsTransaction(M-1002000: 2005-01-10, ADOPTION, 512.00, 1002000, for transfer from 7), |                    33002=CoopAssetsTransaction(M-1002000: 2005-01-10, ADOPTION, 512.00, 1002000, for transfer from 7), | ||||||
|                    34001=CoopAssetsTransaction(M-1002000: 2016-12-31, CLEARING, -8.00, 1002000, for cancellation D), |                    34001=CoopAssetsTransaction(M-1002000: 2016-12-31, CLEARING, -8.00, 1002000, for cancellation D), | ||||||
|                    34002=CoopAssetsTransaction(M-1002000: 2016-12-31, DISBURSAL, -100.00, 1002000, for cancellation D), |                    34002=CoopAssetsTransaction(M-1002000: 2016-12-31, DISBURSAL, -100.00, 1002000, for cancellation D), | ||||||
| @@ -877,21 +877,44 @@ public abstract class BaseOfficeDataImport extends CsvDataImport { | |||||||
|                             .comment(rec.getString("comment")) |                             .comment(rec.getString("comment")) | ||||||
|                             .reference(member.getMemberNumber().toString()) |                             .reference(member.getMemberNumber().toString()) | ||||||
|                             .build(); |                             .build(); | ||||||
|  |  | ||||||
|                     if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.REVERSAL) { |  | ||||||
|                         final var negativeValue = assetTransaction.getAssetValue().negate(); |  | ||||||
|                         final var revertedAssetTx = coopAssets.values().stream().filter(a -> |  | ||||||
|                                         a.getTransactionType() != HsOfficeCoopAssetsTransactionType.REVERSAL && |  | ||||||
|                                                 a.getMembership() == assetTransaction.getMembership() && |  | ||||||
|                                                 a.getAssetValue().equals(negativeValue)) |  | ||||||
|                                 .findAny() |  | ||||||
|                                 .orElseThrow(() -> new IllegalStateException( |  | ||||||
|                                         "cannot determine asset reverse entry for reversal " + assetTransaction)); |  | ||||||
|                         assetTransaction.setRevertedAssetTx(revertedAssetTx); |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction); |                     coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction); | ||||||
|                 }); |                 }); | ||||||
|  |  | ||||||
|  |         coopAssets.values().forEach(assetTransaction -> { | ||||||
|  |             if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.REVERSAL) { | ||||||
|  |                 connectToRelatedRevertedAssetTx(assetTransaction); | ||||||
|  |             } | ||||||
|  |             if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.TRANSFER) { | ||||||
|  |                 connectToRelatedAdoptionAssetTx(assetTransaction); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static void connectToRelatedRevertedAssetTx(final HsOfficeCoopAssetsTransactionEntity assetTransaction) { | ||||||
|  |         final var negativeValue = assetTransaction.getAssetValue().negate(); | ||||||
|  |         final var revertedAssetTx = coopAssets.values().stream().filter(a -> | ||||||
|  |                         a.getTransactionType() != HsOfficeCoopAssetsTransactionType.REVERSAL && | ||||||
|  |                                 a.getMembership() == assetTransaction.getMembership() && | ||||||
|  |                                 a.getAssetValue().equals(negativeValue)) | ||||||
|  |                 .findAny() | ||||||
|  |                 .orElseThrow(() -> new IllegalStateException( | ||||||
|  |                         "cannot determine asset reverse entry for reversal " + assetTransaction)); | ||||||
|  |         assetTransaction.setRevertedAssetTx(revertedAssetTx); | ||||||
|  |         //revertedAssetTx.setAssetReversalTx(assetTransaction); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static void connectToRelatedAdoptionAssetTx(final HsOfficeCoopAssetsTransactionEntity assetTransaction) { | ||||||
|  |         final var negativeValue = assetTransaction.getAssetValue().negate(); | ||||||
|  |         final var adoptionAssetTx = coopAssets.values().stream().filter(a -> | ||||||
|  |                         a.getTransactionType() == HsOfficeCoopAssetsTransactionType.ADOPTION && | ||||||
|  |                                 a.getMembership() != assetTransaction.getMembership() && | ||||||
|  |                                 a.getValueDate().equals(assetTransaction.getValueDate()) && | ||||||
|  |                                 a.getAssetValue().equals(negativeValue)) | ||||||
|  |                 .findAny() | ||||||
|  |                 .orElseThrow(() -> new IllegalStateException( | ||||||
|  |                         "cannot determine asset adoption entry for reversal " + assetTransaction)); | ||||||
|  |         assetTransaction.setAdoptionAssetTx(adoptionAssetTx); | ||||||
|  |         //adoptionAssetTx.setAssetTransferTx(assetTransaction); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static HsOfficeMembershipEntity createOnDemandMembership(final Integer bpId) { |     private static HsOfficeMembershipEntity createOnDemandMembership(final Integer bpId) { | ||||||
|   | |||||||
| @@ -173,8 +173,13 @@ public class CsvDataImport extends ContextBasedTest { | |||||||
|         //System.out.println("persisting #" + entity.hashCode() + ": " + entity); |         //System.out.println("persisting #" + entity.hashCode() + ": " + entity); | ||||||
|         em.persist(entity); |         em.persist(entity); | ||||||
|         // uncomment for debugging purposes |         // uncomment for debugging purposes | ||||||
|         // em.flush(); // makes it slow, but produces better error messages |         // try { | ||||||
|         // System.out.println("persisted #" + entity.hashCode() + " as " + entity.getUuid()); |         //     em.flush(); // makes it slow, but produces better error messages | ||||||
|  |         //     System.out.println("persisted #" + entity.hashCode() + " as " + entity.getUuid()); | ||||||
|  |         //     return entity; | ||||||
|  |         // } catch (final Exception exc) { | ||||||
|  |         //     throw exc; // for breakpoints | ||||||
|  |         // } | ||||||
|         return entity; |         return entity; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ class HsOfficeBankAccountControllerRestTest { | |||||||
|     Context contextMock; |     Context contextMock; | ||||||
|  |  | ||||||
|     @MockBean |     @MockBean | ||||||
|  |     @SuppressWarnings("unused") // not used in test, but in controller class | ||||||
|     StandardMapper mapper; |     StandardMapper mapper; | ||||||
|  |  | ||||||
|     @MockBean |     @MockBean | ||||||
|   | |||||||
| @@ -69,7 +69,7 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased | |||||||
|                 .then().log().all().assertThat() |                 .then().log().all().assertThat() | ||||||
|                     .statusCode(200) |                     .statusCode(200) | ||||||
|                     .contentType("application/json") |                     .contentType("application/json") | ||||||
|                     .body("", hasSize(12));  // @formatter:on |                     .body("", hasSize(3*6)); // @formatter:on | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Test |         @Test | ||||||
| @@ -94,14 +94,22 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased | |||||||
|                                 "assetValue": 320.00, |                                 "assetValue": 320.00, | ||||||
|                                 "valueDate": "2010-03-15", |                                 "valueDate": "2010-03-15", | ||||||
|                                 "reference": "ref 1000202-1", |                                 "reference": "ref 1000202-1", | ||||||
|                                 "comment": "initial deposit" |                                 "comment": "initial deposit", | ||||||
|  |                                 "adoptionAssetTx": null, | ||||||
|  |                                 "transferAssetTx": null, | ||||||
|  |                                 "revertedAssetTx": null, | ||||||
|  |                                 "reversalAssetTx": null | ||||||
|                             }, |                             }, | ||||||
|                             { |                             { | ||||||
|                                 "transactionType": "DISBURSAL", |                                 "transactionType": "DISBURSAL", | ||||||
|                                 "assetValue": -128.00, |                                 "assetValue": -128.00, | ||||||
|                                 "valueDate": "2021-09-01", |                                 "valueDate": "2021-09-01", | ||||||
|                                 "reference": "ref 1000202-2", |                                 "reference": "ref 1000202-2", | ||||||
|                                 "comment": "partial disbursal" |                                 "comment": "partial disbursal", | ||||||
|  |                                 "adoptionAssetTx": null, | ||||||
|  |                                 "transferAssetTx": null, | ||||||
|  |                                 "revertedAssetTx": null, | ||||||
|  |                                 "reversalAssetTx": null | ||||||
|                             }, |                             }, | ||||||
|                             { |                             { | ||||||
|                                 "transactionType": "DEPOSIT", |                                 "transactionType": "DEPOSIT", | ||||||
| @@ -109,12 +117,18 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased | |||||||
|                                 "valueDate": "2022-10-20", |                                 "valueDate": "2022-10-20", | ||||||
|                                 "reference": "ref 1000202-3", |                                 "reference": "ref 1000202-3", | ||||||
|                                 "comment": "some loss", |                                 "comment": "some loss", | ||||||
|  |                                 "adoptionAssetTx": null, | ||||||
|  |                                 "transferAssetTx": null, | ||||||
|  |                                 "revertedAssetTx": null, | ||||||
|                                 "reversalAssetTx": { |                                 "reversalAssetTx": { | ||||||
|                                     "transactionType": "REVERSAL", |                                     "transactionType": "REVERSAL", | ||||||
|                                     "assetValue": -128.00, |                                     "assetValue": -128.00, | ||||||
|                                     "valueDate": "2022-10-21", |                                     "valueDate": "2022-10-21", | ||||||
|                                     "reference": "ref 1000202-3", |                                     "reference": "ref 1000202-3", | ||||||
|                                     "comment": "some reversal" |                                     "comment": "some reversal", | ||||||
|  |                                     "adoptionAssetTx.uuid": null, | ||||||
|  |                                     "transferAssetTx.uuid": null, | ||||||
|  |                                     "reversalAssetTx.uuid": null | ||||||
|                                 } |                                 } | ||||||
|                             }, |                             }, | ||||||
|                             { |                             { | ||||||
| @@ -123,13 +137,59 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased | |||||||
|                                 "valueDate": "2022-10-21", |                                 "valueDate": "2022-10-21", | ||||||
|                                 "reference": "ref 1000202-3", |                                 "reference": "ref 1000202-3", | ||||||
|                                 "comment": "some reversal", |                                 "comment": "some reversal", | ||||||
|  |                                 "adoptionAssetTx": null, | ||||||
|  |                                 "transferAssetTx": null, | ||||||
|                                 "revertedAssetTx": { |                                 "revertedAssetTx": { | ||||||
|                                     "transactionType": "DEPOSIT", |                                     "transactionType": "DEPOSIT", | ||||||
|                                     "assetValue": 128.00, |                                     "assetValue": 128.00, | ||||||
|                                     "valueDate": "2022-10-20", |                                     "valueDate": "2022-10-20", | ||||||
|                                     "reference": "ref 1000202-3", |                                     "reference": "ref 1000202-3", | ||||||
|                                     "comment": "some loss" |                                     "comment": "some loss", | ||||||
|                                 } |                                     "adoptionAssetTx.uuid": null, | ||||||
|  |                                     "transferAssetTx.uuid": null, | ||||||
|  |                                     "revertedAssetTx.uuid": null | ||||||
|  |                                 }, | ||||||
|  |                                 "reversalAssetTx": null | ||||||
|  |                             }, | ||||||
|  |                             { | ||||||
|  |                                 "transactionType": "TRANSFER", | ||||||
|  |                                 "assetValue": -192.00, | ||||||
|  |                                 "valueDate": "2023-12-31", | ||||||
|  |                                 "reference": "ref 1000202-3", | ||||||
|  |                                 "comment": "some reversal", | ||||||
|  |                                 "adoptionAssetTx": { | ||||||
|  |                                     "transactionType": "ADOPTION", | ||||||
|  |                                     "assetValue": 192.00, | ||||||
|  |                                     "valueDate": "2023-12-31", | ||||||
|  |                                     "reference": "ref 1000202-3", | ||||||
|  |                                     "comment": "some reversal", | ||||||
|  |                                     "adoptionAssetTx.uuid": null, | ||||||
|  |                                     "revertedAssetTx.uuid": null, | ||||||
|  |                                     "reversalAssetTx.uuid": null | ||||||
|  |                                 }, | ||||||
|  |                                 "transferAssetTx": null, | ||||||
|  |                                 "revertedAssetTx": null, | ||||||
|  |                                 "reversalAssetTx": null | ||||||
|  |                             }, | ||||||
|  |                             { | ||||||
|  |                                 "transactionType": "ADOPTION", | ||||||
|  |                                 "assetValue": 192.00, | ||||||
|  |                                 "valueDate": "2023-12-31", | ||||||
|  |                                 "reference": "ref 1000202-3", | ||||||
|  |                                 "comment": "some reversal", | ||||||
|  |                                 "adoptionAssetTx": null, | ||||||
|  |                                 "transferAssetTx": { | ||||||
|  |                                     "transactionType": "TRANSFER", | ||||||
|  |                                     "assetValue": -192.00, | ||||||
|  |                                     "valueDate": "2023-12-31", | ||||||
|  |                                     "reference": "ref 1000202-3", | ||||||
|  |                                     "comment": "some reversal", | ||||||
|  |                                     "transferAssetTx.uuid": null, | ||||||
|  |                                     "revertedAssetTx.uuid": null, | ||||||
|  |                                     "reversalAssetTx.uuid": null | ||||||
|  |                                 }, | ||||||
|  |                                 "revertedAssetTx": null, | ||||||
|  |                                 "reversalAssetTx": null | ||||||
|                             } |                             } | ||||||
|                         ] |                         ] | ||||||
|                         """)); // @formatter:on |                         """)); // @formatter:on | ||||||
|   | |||||||
| @@ -1,50 +1,116 @@ | |||||||
| package net.hostsharing.hsadminng.hs.office.coopassets; | package net.hostsharing.hsadminng.hs.office.coopassets; | ||||||
|  |  | ||||||
|  | import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration; | ||||||
| import net.hostsharing.hsadminng.context.Context; | import net.hostsharing.hsadminng.context.Context; | ||||||
| import net.hostsharing.hsadminng.mapper.StandardMapper; | import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; | ||||||
|  | import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository; | ||||||
|  | import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; | ||||||
|  | import net.hostsharing.hsadminng.mapper.StrictMapper; | ||||||
|  | import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; | ||||||
| import net.hostsharing.hsadminng.rbac.test.JsonBuilder; | import net.hostsharing.hsadminng.rbac.test.JsonBuilder; | ||||||
|  | import net.hostsharing.hsadminng.test.TestUuidGenerator; | ||||||
|  | import org.junit.jupiter.api.BeforeEach; | ||||||
| import org.junit.jupiter.params.ParameterizedTest; | import org.junit.jupiter.params.ParameterizedTest; | ||||||
| import org.junit.jupiter.params.provider.EnumSource; | import org.junit.jupiter.params.provider.EnumSource; | ||||||
|  | import org.junit.runner.RunWith; | ||||||
| import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||||
| import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; | ||||||
| import org.springframework.boot.test.mock.mockito.MockBean; | import org.springframework.boot.test.mock.mockito.MockBean; | ||||||
|  | import org.springframework.context.annotation.Import; | ||||||
| import org.springframework.http.MediaType; | import org.springframework.http.MediaType; | ||||||
|  | import org.springframework.test.context.junit4.SpringRunner; | ||||||
| import org.springframework.test.web.servlet.MockMvc; | import org.springframework.test.web.servlet.MockMvc; | ||||||
| import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; | ||||||
|  |  | ||||||
|  | import java.math.BigDecimal; | ||||||
|  | import java.time.LocalDate; | ||||||
|  | import java.util.Optional; | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
| import java.util.function.Function; | import java.util.function.Function; | ||||||
|  |  | ||||||
| import static net.hostsharing.hsadminng.rbac.test.JsonBuilder.jsonObject; | import static net.hostsharing.hsadminng.rbac.test.JsonBuilder.jsonObject; | ||||||
|  | import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; | ||||||
| import static org.hamcrest.Matchers.is; | import static org.hamcrest.Matchers.is; | ||||||
|  | import static org.mockito.ArgumentMatchers.any; | ||||||
|  | import static org.mockito.ArgumentMatchers.eq; | ||||||
|  | import static org.mockito.Mockito.when; | ||||||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | ||||||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||||||
|  |  | ||||||
| @WebMvcTest(HsOfficeCoopAssetsTransactionController.class) | @WebMvcTest(HsOfficeCoopAssetsTransactionController.class) | ||||||
|  | @Import({ StrictMapper.class, JsonObjectMapperConfiguration.class }) | ||||||
|  | @RunWith(SpringRunner.class) | ||||||
| class HsOfficeCoopAssetsTransactionControllerRestTest { | class HsOfficeCoopAssetsTransactionControllerRestTest { | ||||||
|  |  | ||||||
|  |     private static final UUID UNAVAILABLE_MEMBERSHIP_UUID = TestUuidGenerator.use(0); | ||||||
|  |     private static final String UNAVAILABLE_MEMBER_NUMBER = "M-1234699"; | ||||||
|  |  | ||||||
|  |     private static final UUID ORIGIN_MEMBERSHIP_UUID = TestUuidGenerator.use(1); | ||||||
|  |     private static final String ORIGIN_MEMBER_NUMBER = "M-1111100"; | ||||||
|  |     public final HsOfficeMembershipEntity ORIGIN_TARGET_MEMBER_ENTITY = HsOfficeMembershipEntity.builder() | ||||||
|  |             .uuid(ORIGIN_MEMBERSHIP_UUID) | ||||||
|  |             .partner(HsOfficePartnerEntity.builder() | ||||||
|  |                     .partnerNumber(partnerNumberOf(ORIGIN_MEMBER_NUMBER)) | ||||||
|  |                     .build()) | ||||||
|  |             .memberNumberSuffix(suffixOf(ORIGIN_MEMBER_NUMBER)) | ||||||
|  |             .build(); | ||||||
|  |  | ||||||
|  |     private static final UUID AVAILABLE_TARGET_MEMBERSHIP_UUID = TestUuidGenerator.use(2); | ||||||
|  |     private static final String AVAILABLE_TARGET_MEMBER_NUMBER = "M-1234500"; | ||||||
|  |     public final HsOfficeMembershipEntity AVAILABLE_MEMBER_ENTITY = HsOfficeMembershipEntity.builder() | ||||||
|  |             .uuid(AVAILABLE_TARGET_MEMBERSHIP_UUID) | ||||||
|  |             .partner(HsOfficePartnerEntity.builder() | ||||||
|  |                     .partnerNumber(partnerNumberOf(AVAILABLE_TARGET_MEMBER_NUMBER)) | ||||||
|  |                     .build()) | ||||||
|  |             .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); | ||||||
|  |  | ||||||
|  |     private static final UUID SOME_EXISTING_LOSS_ASSET_TX_UUID = TestUuidGenerator.use(3); | ||||||
|  |     public final HsOfficeCoopAssetsTransactionEntity SOME_EXISTING_LOSS_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder() | ||||||
|  |             .uuid(SOME_EXISTING_LOSS_ASSET_TX_UUID) | ||||||
|  |             .membership(ORIGIN_TARGET_MEMBER_ENTITY) | ||||||
|  |             .transactionType(HsOfficeCoopAssetsTransactionType.LOSS) | ||||||
|  |             .assetValue(BigDecimal.valueOf(-64)) | ||||||
|  |             .reference("some loss asset tx ref") | ||||||
|  |             .comment("some loss asset tx comment") | ||||||
|  |             .valueDate(LocalDate.parse("2024-10-15")) | ||||||
|  |             .build(); | ||||||
|  |  | ||||||
|     @Autowired |     @Autowired | ||||||
|     MockMvc mockMvc; |     MockMvc mockMvc; | ||||||
|  |  | ||||||
|     @MockBean |     @MockBean | ||||||
|     Context contextMock; |     Context contextMock; | ||||||
|  |  | ||||||
|  |     @Autowired | ||||||
|  |     @SuppressWarnings("unused") // not used in test, but in controller class | ||||||
|  |     StrictMapper mapper; | ||||||
|  |  | ||||||
|     @MockBean |     @MockBean | ||||||
|     StandardMapper mapper; |     EntityManagerWrapper emw; // even if not used in test anymore, it's needed by base-class of StrictMapper | ||||||
|  |  | ||||||
|     @MockBean |     @MockBean | ||||||
|     HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo; |     HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo; | ||||||
|  |  | ||||||
|     static final String VALID_INSERT_REQUEST_BODY = """ |     @MockBean | ||||||
|  |     HsOfficeMembershipRepository membershipRepo; | ||||||
|  |  | ||||||
|  |     static final String INSERT_REQUEST_BODY_TEMPLATE = """ | ||||||
|             { |             { | ||||||
|                "membership.uuid": "%s", |                "membership.uuid": "%s", | ||||||
|                "transactionType": "DEPOSIT", |                "transactionType": "DEPOSIT", | ||||||
|                "assetValue": 128.00, |                "assetValue": 128.00, | ||||||
|                "valueDate": "2022-10-13", |                "valueDate": "2022-10-13", | ||||||
|                "reference": "valid reference", |                "reference": "valid reference", | ||||||
|                "comment": "valid comment" |                "comment": "valid comment", | ||||||
|  |                "adoptingMembership.uuid": null, | ||||||
|  |                "adoptingMembership.memberNumber": null | ||||||
|             } |             } | ||||||
|             """.formatted(UUID.randomUUID()); |             """.formatted(ORIGIN_MEMBERSHIP_UUID); | ||||||
|  |  | ||||||
|     enum BadRequestTestCases { |     enum BadRequestTestCases { | ||||||
|         MEMBERSHIP_UUID_MISSING( |         MEMBERSHIP_UUID_MISSING( | ||||||
| @@ -65,8 +131,6 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { | |||||||
|                         .with("assetValue", -64.00), |                         .with("assetValue", -64.00), | ||||||
|                 "[for DEPOSIT, assetValue must be positive but is \"-64.00\"]"), |                 "[for DEPOSIT, assetValue must be positive but is \"-64.00\"]"), | ||||||
|  |  | ||||||
|         //TODO: other transaction types |  | ||||||
|  |  | ||||||
|         ASSETS_VALUE_FOR_DISBURSAL_MUST_BE_NEGATIVE( |         ASSETS_VALUE_FOR_DISBURSAL_MUST_BE_NEGATIVE( | ||||||
|                 requestBody -> requestBody |                 requestBody -> requestBody | ||||||
|                         .with("transactionType", "DISBURSAL") |                         .with("transactionType", "DISBURSAL") | ||||||
| @@ -75,6 +139,20 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { | |||||||
|  |  | ||||||
|         //TODO: other transaction types |         //TODO: other transaction types | ||||||
|  |  | ||||||
|  |         ADOPTING_MEMBERSHIP_NUMBER_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE( | ||||||
|  |                 requestBody -> requestBody | ||||||
|  |                         .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("assetValue", -64.00) | ||||||
|  |                         .with("adoptingMembership.uuid", UNAVAILABLE_MEMBERSHIP_UUID.toString()), | ||||||
|  |                 "adoptingMembership.uuid='" + UNAVAILABLE_MEMBERSHIP_UUID + "' not found or not accessible"), | ||||||
|  |  | ||||||
|         ASSETS_VALUE_MUST_NOT_BE_NULL( |         ASSETS_VALUE_MUST_NOT_BE_NULL( | ||||||
|                 requestBody -> requestBody |                 requestBody -> requestBody | ||||||
|                         .with("transactionType", "REVERSAL") |                         .with("transactionType", "REVERSAL") | ||||||
| @@ -104,13 +182,16 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         String givenRequestBody() { |         String givenRequestBody() { | ||||||
|             return givenBodyTransformation.apply(jsonObject(VALID_INSERT_REQUEST_BODY)).toString(); |             return givenBodyTransformation.apply(jsonObject(INSERT_REQUEST_BODY_TEMPLATE)).toString(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @ParameterizedTest |     @ParameterizedTest | ||||||
|     @EnumSource(BadRequestTestCases.class) |     @EnumSource(BadRequestTestCases.class) | ||||||
|     void respondWithBadRequest(final BadRequestTestCases testCase) throws Exception { |     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(); | ||||||
|  |  | ||||||
|         // when |         // when | ||||||
|         mockMvc.perform(MockMvcRequestBuilders |         mockMvc.perform(MockMvcRequestBuilders | ||||||
| @@ -127,4 +208,160 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { | |||||||
|                 .andExpect(status().is4xxClientError()); |                 .andExpect(status().is4xxClientError()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     enum SuccessfullyCreatedTestCases { | ||||||
|  |  | ||||||
|  |         REVERTING_SIMPLE_ASSET_TRANSACTION( | ||||||
|  |                 requestBody -> requestBody | ||||||
|  |                         .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), | ||||||
|  |  | ||||||
|  |         TRANSFER_TO_GIVEN_AVAILABLE_MEMBERSHIP_NUMBER( | ||||||
|  |                 requestBody -> requestBody | ||||||
|  |                         .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("assetValue", -64.00) | ||||||
|  |                         .with("membership.uuid", ORIGIN_MEMBERSHIP_UUID.toString()) | ||||||
|  |                         .with("adoptingMembership.uuid", AVAILABLE_TARGET_MEMBERSHIP_UUID.toString()), | ||||||
|  |                 Expected.TRANSFER_RESPONSE); | ||||||
|  |  | ||||||
|  |         private final Function<JsonBuilder, JsonBuilder> givenBodyTransformation; | ||||||
|  |         private final String expectedResponseBody; | ||||||
|  |  | ||||||
|  |         SuccessfullyCreatedTestCases( | ||||||
|  |                 final Function<JsonBuilder, JsonBuilder> givenBodyTransformation, | ||||||
|  |                 final String expectedResponseBody) { | ||||||
|  |             this.givenBodyTransformation = givenBodyTransformation; | ||||||
|  |             this.expectedResponseBody = expectedResponseBody; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         String givenRequestBody() { | ||||||
|  |             return givenBodyTransformation.apply(jsonObject(INSERT_REQUEST_BODY_TEMPLATE)).toString(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private static class Expected { | ||||||
|  |  | ||||||
|  |             public static final String REVERT_RESPONSE = """ | ||||||
|  |                     { | ||||||
|  |                          "uuid": "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}", | ||||||
|  |                          "membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}", | ||||||
|  |                          "membership.memberNumber": "%{ORIGIN_MEMBER_NUMBER}", | ||||||
|  |                          "transactionType": "REVERSAL", | ||||||
|  |                          "assetValue": 64.00, | ||||||
|  |                          "valueDate": "2024-10-15", | ||||||
|  |                          "reference": "reversal ref", | ||||||
|  |                          "comment": "reversal comment", | ||||||
|  |                          "adoptionAssetTx": null, | ||||||
|  |                          "transferAssetTx": null, | ||||||
|  |                          "revertedAssetTx": { | ||||||
|  |                            "uuid": "%{SOME_EXISTING_LOSS_ASSET_TX_UUID}", | ||||||
|  |                            "membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}", | ||||||
|  |                            "membership.memberNumber": "%{ORIGIN_MEMBER_NUMBER}", | ||||||
|  |                            "transactionType": "LOSS", | ||||||
|  |                            "assetValue": -64.00, | ||||||
|  |                            "valueDate": "2024-10-15", | ||||||
|  |                            "reference": "some loss asset tx ref", | ||||||
|  |                            "comment": "some loss asset tx comment", | ||||||
|  |                            "adoptionAssetTx.uuid": null, | ||||||
|  |                            "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_LOSS_ASSET_TX_UUID}", SOME_EXISTING_LOSS_ASSET_TX_UUID.toString()); | ||||||
|  |  | ||||||
|  |             public static final String TRANSFER_RESPONSE = """ | ||||||
|  |                     { | ||||||
|  |                         "uuid": "%{NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID}", | ||||||
|  |                         "membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}", | ||||||
|  |                         "membership.memberNumber": "%{ORIGIN_MEMBER_NUMBER}", | ||||||
|  |                         "transactionType": "TRANSFER", | ||||||
|  |                         "assetValue": -64.00, | ||||||
|  |                         "adoptionAssetTx": { | ||||||
|  |                             "membership.uuid": "%{AVAILABLE_MEMBERSHIP_UUID}", | ||||||
|  |                             "membership.memberNumber": "%{AVAILABLE_TARGET_MEMBER_NUMBER}", | ||||||
|  |                             "transactionType": "ADOPTION", | ||||||
|  |                             "assetValue": 64.00, | ||||||
|  |                             "transferAssetTx.uuid": "%{NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID}" | ||||||
|  |                         }, | ||||||
|  |                         "transferAssetTx": null, | ||||||
|  |                         "revertedAssetTx": null, | ||||||
|  |                         "reversalAssetTx": null | ||||||
|  |                     } | ||||||
|  |                     """ | ||||||
|  |                     .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); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @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(); | ||||||
|  |  | ||||||
|  |         // when | ||||||
|  |         mockMvc.perform(MockMvcRequestBuilders | ||||||
|  |                         .post("/api/hs/office/coopassetstransactions") | ||||||
|  |                         .header("current-subject", "superuser-alex@hostsharing.net") | ||||||
|  |                         .contentType(MediaType.APPLICATION_JSON) | ||||||
|  |                         .content(testCase.givenRequestBody()) | ||||||
|  |                         .accept(MediaType.APPLICATION_JSON)) | ||||||
|  |  | ||||||
|  |                 // then | ||||||
|  |                 .andExpect(status().is2xxSuccessful()) | ||||||
|  |                 .andExpect(jsonPath("$", lenientlyEquals(testCase.expectedResponseBody))); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @BeforeEach | ||||||
|  |     void initMocks() { | ||||||
|  |         TestUuidGenerator.start(4); | ||||||
|  |  | ||||||
|  |         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); | ||||||
|  |  | ||||||
|  |         final var availableMemberNumber = Integer.valueOf(AVAILABLE_TARGET_MEMBER_NUMBER.substring("M-".length())); | ||||||
|  |         when(membershipRepo.findMembershipByMemberNumber(eq(availableMemberNumber))).thenReturn(AVAILABLE_MEMBER_ENTITY); | ||||||
|  |  | ||||||
|  |         when(membershipRepo.findByUuid(eq(ORIGIN_MEMBERSHIP_UUID))).thenReturn(Optional.of(ORIGIN_TARGET_MEMBER_ENTITY)); | ||||||
|  |         when(membershipRepo.findByUuid(eq(AVAILABLE_TARGET_MEMBERSHIP_UUID))).thenReturn(Optional.of(AVAILABLE_MEMBER_ENTITY)); | ||||||
|  |  | ||||||
|  |         when(coopAssetsTransactionRepo.findByUuid(SOME_EXISTING_LOSS_ASSET_TX_UUID)) | ||||||
|  |                 .thenReturn(Optional.of(SOME_EXISTING_LOSS_ASSET_TX_ENTITY)); | ||||||
|  |         when(coopAssetsTransactionRepo.save(any(HsOfficeCoopAssetsTransactionEntity.class))) | ||||||
|  |                 .thenAnswer(invocation -> { | ||||||
|  |                             final var entity = (HsOfficeCoopAssetsTransactionEntity) invocation.getArgument(0); | ||||||
|  |                             if (entity.getUuid() == null) { | ||||||
|  |                                 entity.setUuid(TestUuidGenerator.next()); | ||||||
|  |                             } | ||||||
|  |                             return entity; | ||||||
|  |                         } | ||||||
|  |                 ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private int partnerNumberOf(final String memberNumber) { | ||||||
|  |         return Integer.parseInt(memberNumber.substring("M-".length(), memberNumber.length()-2)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private String suffixOf(final String memberNumber) { | ||||||
|  |         return memberNumber.substring("M-".length()+5); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -20,7 +20,6 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest { | |||||||
|             .comment("some comment") |             .comment("some comment") | ||||||
|             .build(); |             .build(); | ||||||
|  |  | ||||||
|  |  | ||||||
|     final HsOfficeCoopAssetsTransactionEntity givenCoopAssetReversalTransaction = HsOfficeCoopAssetsTransactionEntity.builder() |     final HsOfficeCoopAssetsTransactionEntity givenCoopAssetReversalTransaction = HsOfficeCoopAssetsTransactionEntity.builder() | ||||||
|             .membership(TEST_MEMBERSHIP) |             .membership(TEST_MEMBERSHIP) | ||||||
|             .reference("some-ref") |             .reference("some-ref") | ||||||
| @@ -31,6 +30,16 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest { | |||||||
|             .revertedAssetTx(givenCoopAssetTransaction) |             .revertedAssetTx(givenCoopAssetTransaction) | ||||||
|             .build(); |             .build(); | ||||||
|  |  | ||||||
|  |     final HsOfficeCoopAssetsTransactionEntity givenAdoptedCoopAssetTransaction = HsOfficeCoopAssetsTransactionEntity.builder() | ||||||
|  |             .membership(TEST_MEMBERSHIP) | ||||||
|  |             .reference("some-ref") | ||||||
|  |             .valueDate(LocalDate.parse("2020-01-15")) | ||||||
|  |             .transactionType(HsOfficeCoopAssetsTransactionType.ADOPTION) | ||||||
|  |             .assetValue(new BigDecimal("128.00")) | ||||||
|  |             .comment("some comment") | ||||||
|  |             .revertedAssetTx(givenCoopAssetTransaction) | ||||||
|  |             .build(); | ||||||
|  |  | ||||||
|     final HsOfficeCoopAssetsTransactionEntity givenEmptyCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder().build(); |     final HsOfficeCoopAssetsTransactionEntity givenEmptyCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder().build(); | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
| @@ -49,6 +58,15 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest { | |||||||
|         assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref, some comment, M-1000101:REV:-128.00)"); |         assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref, some comment, M-1000101:REV:-128.00)"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     void toStringWithAdoptedAssetTxContainsRevertedAssetTx() { | ||||||
|  |         givenCoopAssetTransaction.setAdoptionAssetTx(givenAdoptedCoopAssetTransaction); | ||||||
|  |  | ||||||
|  |         final var result = givenCoopAssetTransaction.toString(); | ||||||
|  |  | ||||||
|  |         assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref, some comment, M-1000101:ADO:+128.00)"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     void toShortStringContainsOnlyMemberNumberSuffixAndSharesCountOnly() { |     void toShortStringContainsOnlyMemberNumberSuffixAndSharesCountOnly() { | ||||||
|         final var result = givenCoopAssetTransaction.toShortString(); |         final var result = givenCoopAssetTransaction.toShortString(); | ||||||
|   | |||||||
| @@ -144,16 +144,22 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase | |||||||
|                     "CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)", |                     "CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)", | ||||||
|                     "CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:REV:-128.00)", |                     "CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:REV:-128.00)", | ||||||
|                     "CoopAssetsTransaction(M-1000101: 2022-10-21, REVERSAL, -128.00, ref 1000101-3, some reversal, M-1000101:DEP:+128.00)", |                     "CoopAssetsTransaction(M-1000101: 2022-10-21, REVERSAL, -128.00, ref 1000101-3, some reversal, M-1000101:DEP:+128.00)", | ||||||
|  |                     "CoopAssetsTransaction(M-1000101: 2023-12-31, ADOPTION, 192.00, ref 1000101-3, some reversal, M-1000101:TRA:-192.00)", | ||||||
|  |                     "CoopAssetsTransaction(M-1000101: 2023-12-31, TRANSFER, -192.00, ref 1000101-3, some reversal, M-1000101:ADO:+192.00)", | ||||||
|  |  | ||||||
|                     "CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)", |                     "CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)", | ||||||
|                     "CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)", |                     "CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)", | ||||||
|                     "CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:REV:-128.00)", |                     "CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:REV:-128.00)", | ||||||
|                     "CoopAssetsTransaction(M-1000202: 2022-10-21, REVERSAL, -128.00, ref 1000202-3, some reversal, M-1000202:DEP:+128.00)", |                     "CoopAssetsTransaction(M-1000202: 2022-10-21, REVERSAL, -128.00, ref 1000202-3, some reversal, M-1000202:DEP:+128.00)", | ||||||
|  |                     "CoopAssetsTransaction(M-1000202: 2023-12-31, TRANSFER, -192.00, ref 1000202-3, some reversal, M-1000202:ADO:+192.00)", | ||||||
|  |                     "CoopAssetsTransaction(M-1000202: 2023-12-31, ADOPTION, 192.00, ref 1000202-3, some reversal, M-1000202:TRA:-192.00)", | ||||||
|  |  | ||||||
|                     "CoopAssetsTransaction(M-1000303: 2010-03-15, DEPOSIT, 320.00, ref 1000303-1, initial deposit)", |                     "CoopAssetsTransaction(M-1000303: 2010-03-15, DEPOSIT, 320.00, ref 1000303-1, initial deposit)", | ||||||
|                     "CoopAssetsTransaction(M-1000303: 2021-09-01, DISBURSAL, -128.00, ref 1000303-2, partial disbursal)", |                     "CoopAssetsTransaction(M-1000303: 2021-09-01, DISBURSAL, -128.00, ref 1000303-2, partial disbursal)", | ||||||
|                     "CoopAssetsTransaction(M-1000303: 2022-10-20, DEPOSIT, 128.00, ref 1000303-3, some loss, M-1000303:REV:-128.00)", |                     "CoopAssetsTransaction(M-1000303: 2022-10-20, DEPOSIT, 128.00, ref 1000303-3, some loss, M-1000303:REV:-128.00)", | ||||||
|                     "CoopAssetsTransaction(M-1000303: 2022-10-21, REVERSAL, -128.00, ref 1000303-3, some reversal, M-1000303:DEP:+128.00)"); |                     "CoopAssetsTransaction(M-1000303: 2022-10-21, REVERSAL, -128.00, ref 1000303-3, some reversal, M-1000303:DEP:+128.00)", | ||||||
|  |                     "CoopAssetsTransaction(M-1000303: 2023-12-31, TRANSFER, -192.00, ref 1000303-3, some reversal, M-1000303:ADO:+192.00)", | ||||||
|  |                     "CoopAssetsTransaction(M-1000303: 2023-12-31, ADOPTION, 192.00, ref 1000303-3, some reversal, M-1000303:TRA:-192.00)"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Test |         @Test | ||||||
| @@ -174,7 +180,9 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase | |||||||
|                     "CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)", |                     "CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)", | ||||||
|                     "CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)", |                     "CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)", | ||||||
|                     "CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:REV:-128.00)", |                     "CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:REV:-128.00)", | ||||||
|                     "CoopAssetsTransaction(M-1000202: 2022-10-21, REVERSAL, -128.00, ref 1000202-3, some reversal, M-1000202:DEP:+128.00)"); |                     "CoopAssetsTransaction(M-1000202: 2022-10-21, REVERSAL, -128.00, ref 1000202-3, some reversal, M-1000202:DEP:+128.00)", | ||||||
|  |                     "CoopAssetsTransaction(M-1000202: 2023-12-31, TRANSFER, -192.00, ref 1000202-3, some reversal, M-1000202:ADO:+192.00)", | ||||||
|  |                     "CoopAssetsTransaction(M-1000202: 2023-12-31, ADOPTION, 192.00, ref 1000202-3, some reversal, M-1000202:TRA:-192.00)"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Test |         @Test | ||||||
| @@ -212,7 +220,9 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase | |||||||
|                     "CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)", |                     "CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)", | ||||||
|                     "CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)", |                     "CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)", | ||||||
|                     "CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:REV:-128.00)", |                     "CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:REV:-128.00)", | ||||||
|                     "CoopAssetsTransaction(M-1000101: 2022-10-21, REVERSAL, -128.00, ref 1000101-3, some reversal, M-1000101:DEP:+128.00)"); |                     "CoopAssetsTransaction(M-1000101: 2022-10-21, REVERSAL, -128.00, ref 1000101-3, some reversal, M-1000101:DEP:+128.00)", | ||||||
|  |                     "CoopAssetsTransaction(M-1000101: 2023-12-31, TRANSFER, -192.00, ref 1000101-3, some reversal, M-1000101:ADO:+192.00)", | ||||||
|  |                     "CoopAssetsTransaction(M-1000101: 2023-12-31, ADOPTION, 192.00, ref 1000101-3, some reversal, M-1000101:TRA:-192.00)"); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ class HsOfficeCoopSharesTransactionControllerRestTest { | |||||||
|     Context contextMock; |     Context contextMock; | ||||||
|  |  | ||||||
|     @MockBean |     @MockBean | ||||||
|  |     @SuppressWarnings("unused") // not used in test, but in controller class | ||||||
|     StandardMapper mapper; |     StandardMapper mapper; | ||||||
|  |  | ||||||
|     @MockBean |     @MockBean | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ class HsOfficeCoopSharesTransactionEntityUnitTest { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     void toStringWithRevertedAssetTxContainsRevertedAssetTx() { |     void toStringWithRelatedAssetTxContainsRelatedAssetTx() { | ||||||
|         givenCoopSharesTransaction.setRevertedShareTx(givenCoopShareReversalTransaction); |         givenCoopSharesTransaction.setRevertedShareTx(givenCoopShareReversalTransaction); | ||||||
|  |  | ||||||
|         final var result = givenCoopSharesTransaction.toString(); |         final var result = givenCoopSharesTransaction.toString(); | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ 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.CreateCoopAssetsDepositTransaction; | ||||||
| import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDisbursalTransaction; | 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.CreateCoopAssetsRevertTransaction; | ||||||
|  | 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.CreateCoopSharesCancellationTransaction; | ||||||
| import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesRevertTransaction; | 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.membership.coopshares.CreateCoopSharesSubscriptionTransaction; | ||||||
| @@ -29,12 +30,15 @@ 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.SubscribeToMailinglist; | ||||||
| import net.hostsharing.hsadminng.hs.office.scenarios.subscription.UnsubscribeFromMailinglist; | import net.hostsharing.hsadminng.hs.office.scenarios.subscription.UnsubscribeFromMailinglist; | ||||||
| import net.hostsharing.hsadminng.rbac.test.JpaAttempt; | 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.Disabled; | ||||||
| import org.junit.jupiter.api.MethodOrderer; | import org.junit.jupiter.api.MethodOrderer; | ||||||
| import org.junit.jupiter.api.Order; | import org.junit.jupiter.api.Order; | ||||||
| import org.junit.jupiter.api.Tag; | import org.junit.jupiter.api.Tag; | ||||||
| import org.junit.jupiter.api.Test; | import org.junit.jupiter.api.Test; | ||||||
| import org.junit.jupiter.api.TestMethodOrder; | import org.junit.jupiter.api.TestMethodOrder; | ||||||
|  | import org.junit.jupiter.api.extension.ExtendWith; | ||||||
| import org.springframework.boot.test.context.SpringBootTest; | import org.springframework.boot.test.context.SpringBootTest; | ||||||
| import org.springframework.test.annotation.DirtiesContext; | import org.springframework.test.annotation.DirtiesContext; | ||||||
|  |  | ||||||
| @@ -51,6 +55,7 @@ import org.springframework.test.annotation.DirtiesContext; | |||||||
| ) | ) | ||||||
| @DirtiesContext | @DirtiesContext | ||||||
| @TestMethodOrder(MethodOrderer.OrderAnnotation.class) | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) | ||||||
|  | @ExtendWith(IgnoreOnFailureExtension.class) | ||||||
| class HsOfficeScenarioTests extends ScenarioTest { | class HsOfficeScenarioTests extends ScenarioTest { | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
| @@ -77,8 +82,8 @@ class HsOfficeScenarioTests extends ScenarioTest { | |||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     @Order(1011) |     @Order(1011) | ||||||
|     @Produces(explicitly = "Partner: P-31011 - Michelle Matthieu", implicitly = { "Person: Michelle Matthieu", |     @Produces(explicitly = "Partner: P-31011 - Michelle Matthieu", | ||||||
|                                                                                   "Contact: Michelle Matthieu" }) |             implicitly = { "Person: Michelle Matthieu", "Contact: Michelle Matthieu" }) | ||||||
|     void shouldCreateNaturalPersonAsPartner() { |     void shouldCreateNaturalPersonAsPartner() { | ||||||
|         new CreatePartner(this) |         new CreatePartner(this) | ||||||
|                 .given("partnerNumber", "P-31011") |                 .given("partnerNumber", "P-31011") | ||||||
| @@ -336,7 +341,7 @@ class HsOfficeScenarioTests extends ScenarioTest { | |||||||
|     @Test |     @Test | ||||||
|     @Order(4201) |     @Order(4201) | ||||||
|     @Requires("Membership: M-3101000 - Test AG") |     @Requires("Membership: M-3101000 - Test AG") | ||||||
|     @Produces("Coop-Shares SUBSCRIPTION Transaction") |     @Produces("Coop-Shares M-3101000 - Test AG - SUBSCRIPTION Transaction") | ||||||
|     void shouldSubscribeCoopShares() { |     void shouldSubscribeCoopShares() { | ||||||
|         new CreateCoopSharesSubscriptionTransaction(this) |         new CreateCoopSharesSubscriptionTransaction(this) | ||||||
|                 .given("memberNumber", "M-3101000") |                 .given("memberNumber", "M-3101000") | ||||||
| @@ -360,8 +365,8 @@ class HsOfficeScenarioTests extends ScenarioTest { | |||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     @Order(4202) |     @Order(4202) | ||||||
|     @Requires("Coop-Shares SUBSCRIPTION Transaction") |     @Requires("Coop-Shares M-3101000 - Test AG - SUBSCRIPTION Transaction") | ||||||
|     @Produces("Coop-Shares CANCELLATION Transaction") |     @Produces("Coop-Shares M-3101000 - Test AG - CANCELLATION Transaction") | ||||||
|     void shouldCancelCoopSharesSubscription() { |     void shouldCancelCoopSharesSubscription() { | ||||||
|         new CreateCoopSharesCancellationTransaction(this) |         new CreateCoopSharesCancellationTransaction(this) | ||||||
|                 .given("memberNumber", "M-3101000") |                 .given("memberNumber", "M-3101000") | ||||||
| @@ -375,7 +380,7 @@ class HsOfficeScenarioTests extends ScenarioTest { | |||||||
|     @Test |     @Test | ||||||
|     @Order(4301) |     @Order(4301) | ||||||
|     @Requires("Membership: M-3101000 - Test AG") |     @Requires("Membership: M-3101000 - Test AG") | ||||||
|     @Produces("Coop-Assets DEPOSIT Transaction") |     @Produces("Coop-Assets M-3101000 - Test AG - DEPOSIT Transaction") | ||||||
|     void shouldSubscribeCoopAssets() { |     void shouldSubscribeCoopAssets() { | ||||||
|         new CreateCoopAssetsDepositTransaction(this) |         new CreateCoopAssetsDepositTransaction(this) | ||||||
|                 .given("memberNumber", "M-3101000") |                 .given("memberNumber", "M-3101000") | ||||||
| @@ -388,7 +393,7 @@ class HsOfficeScenarioTests extends ScenarioTest { | |||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     @Order(4302) |     @Order(4302) | ||||||
|     @Requires("Coop-Assets DEPOSIT Transaction") |     @Requires("Membership: M-3101000 - Test AG") | ||||||
|     void shouldRevertCoopAssetsSubscription() { |     void shouldRevertCoopAssetsSubscription() { | ||||||
|         new CreateCoopAssetsRevertTransaction(this) |         new CreateCoopAssetsRevertTransaction(this) | ||||||
|                 .given("memberNumber", "M-3101000") |                 .given("memberNumber", "M-3101000") | ||||||
| @@ -398,9 +403,9 @@ class HsOfficeScenarioTests extends ScenarioTest { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     @Order(4302) |     @Order(4303) | ||||||
|     @Requires("Coop-Assets DEPOSIT Transaction") |     @Requires("Coop-Assets M-3101000 - Test AG - DEPOSIT Transaction") | ||||||
|     @Produces("Coop-Assets DISBURSAL Transaction") |     @Produces("Coop-Assets M-3101000 - Test AG - DISBURSAL Transaction") | ||||||
|     void shouldDisburseCoopAssets() { |     void shouldDisburseCoopAssets() { | ||||||
|         new CreateCoopAssetsDisbursalTransaction(this) |         new CreateCoopAssetsDisbursalTransaction(this) | ||||||
|                 .given("memberNumber", "M-3101000") |                 .given("memberNumber", "M-3101000") | ||||||
| @@ -411,6 +416,33 @@ class HsOfficeScenarioTests extends ScenarioTest { | |||||||
|                 .doRun(); |                 .doRun(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     @Order(4304) | ||||||
|  |     @Requires("Coop-Assets M-3101000 - Test AG - DEPOSIT Transaction") | ||||||
|  |     @Produces("Coop-Assets M-3101000 - Test AG - TRANSFER Transaction") | ||||||
|  |     void shouldTransferCoopAssets() { | ||||||
|  |         new CreateCoopAssetsTransferTransaction(this) | ||||||
|  |                 .given("transferringMemberNumber", "M-3101000") | ||||||
|  |                 .given("adoptingMemberNumber", "M-4303000") | ||||||
|  |                 .given("reference", "transfer 2024-12-31") | ||||||
|  |                 .given("valueToDisburse", 2 * 64) | ||||||
|  |                 .given("comment", "transfer assets from M-3101000 to M-4303000") | ||||||
|  |                 .given("transactionDate", "2024-12-31") | ||||||
|  |                 .doRun(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @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") | ||||||
|  |                 .given("comment", "reverting some incorrect transfer transaction") | ||||||
|  |                 .given("dateOfIncorrectTransaction", "2024-02-15") | ||||||
|  |                 .doRun(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     @Order(4900) |     @Order(4900) | ||||||
|     @Requires("Membership: M-3101000 - Test AG") |     @Requires("Membership: M-3101000 - Test AG") | ||||||
|   | |||||||
| @@ -36,6 +36,7 @@ import java.util.function.Supplier; | |||||||
| import static java.net.URLEncoder.encode; | import static java.net.URLEncoder.encode; | ||||||
| import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; | 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.hs.office.scenarios.TemplateResolver.Resolver.KEEP_COMMENTS; | ||||||
|  | import static net.hostsharing.hsadminng.test.DebuggerDetection.isDebuggerAttached; | ||||||
| import static org.assertj.core.api.Assertions.assertThat; | import static org.assertj.core.api.Assertions.assertThat; | ||||||
| import static org.junit.jupiter.api.Assertions.fail; | import static org.junit.jupiter.api.Assertions.fail; | ||||||
| import static org.junit.platform.commons.util.StringUtils.isBlank; | import static org.junit.platform.commons.util.StringUtils.isBlank; | ||||||
| @@ -151,7 +152,7 @@ public abstract class UseCase<T extends UseCase<?>> { | |||||||
|                 .GET() |                 .GET() | ||||||
|                 .uri(new URI("http://localhost:" + testSuite.port + uriPath)) |                 .uri(new URI("http://localhost:" + testSuite.port + uriPath)) | ||||||
|                 .header("current-subject", ScenarioTest.RUN_AS_USER) |                 .header("current-subject", ScenarioTest.RUN_AS_USER) | ||||||
|                 .timeout(Duration.ofSeconds(10)) |                 .timeout(seconds(10)) | ||||||
|                 .build(); |                 .build(); | ||||||
|         final var response = client.send(request, BodyHandlers.ofString()); |         final var response = client.send(request, BodyHandlers.ofString()); | ||||||
|         return new HttpResponse(HttpMethod.GET, uriPath, null, response); |         return new HttpResponse(HttpMethod.GET, uriPath, null, response); | ||||||
| @@ -166,7 +167,7 @@ public abstract class UseCase<T extends UseCase<?>> { | |||||||
|                 .uri(new URI("http://localhost:" + testSuite.port + uriPath)) |                 .uri(new URI("http://localhost:" + testSuite.port + uriPath)) | ||||||
|                 .header("Content-Type", "application/json") |                 .header("Content-Type", "application/json") | ||||||
|                 .header("current-subject", ScenarioTest.RUN_AS_USER) |                 .header("current-subject", ScenarioTest.RUN_AS_USER) | ||||||
|                 .timeout(Duration.ofSeconds(10)) |                 .timeout(seconds(10)) | ||||||
|                 .build(); |                 .build(); | ||||||
|         final var response = client.send(request, BodyHandlers.ofString()); |         final var response = client.send(request, BodyHandlers.ofString()); | ||||||
|         return new HttpResponse(HttpMethod.POST, uriPath, requestBody, response); |         return new HttpResponse(HttpMethod.POST, uriPath, requestBody, response); | ||||||
| @@ -181,7 +182,7 @@ public abstract class UseCase<T extends UseCase<?>> { | |||||||
|                 .uri(new URI("http://localhost:" + testSuite.port + uriPath)) |                 .uri(new URI("http://localhost:" + testSuite.port + uriPath)) | ||||||
|                 .header("Content-Type", "application/json") |                 .header("Content-Type", "application/json") | ||||||
|                 .header("current-subject", ScenarioTest.RUN_AS_USER) |                 .header("current-subject", ScenarioTest.RUN_AS_USER) | ||||||
|                 .timeout(Duration.ofSeconds(10)) |                 .timeout(seconds(10)) | ||||||
|                 .build(); |                 .build(); | ||||||
|         final var response = client.send(request, BodyHandlers.ofString()); |         final var response = client.send(request, BodyHandlers.ofString()); | ||||||
|         return new HttpResponse(HttpMethod.PATCH, uriPath, requestBody, response); |         return new HttpResponse(HttpMethod.PATCH, uriPath, requestBody, response); | ||||||
| @@ -195,7 +196,7 @@ public abstract class UseCase<T extends UseCase<?>> { | |||||||
|                 .uri(new URI("http://localhost:" + testSuite.port + uriPath)) |                 .uri(new URI("http://localhost:" + testSuite.port + uriPath)) | ||||||
|                 .header("Content-Type", "application/json") |                 .header("Content-Type", "application/json") | ||||||
|                 .header("current-subject", ScenarioTest.RUN_AS_USER) |                 .header("current-subject", ScenarioTest.RUN_AS_USER) | ||||||
|                 .timeout(Duration.ofSeconds(10)) |                 .timeout(seconds(10)) | ||||||
|                 .build(); |                 .build(); | ||||||
|         final var response = client.send(request, BodyHandlers.ofString()); |         final var response = client.send(request, BodyHandlers.ofString()); | ||||||
|         return new HttpResponse(HttpMethod.DELETE, uriPath, null, response); |         return new HttpResponse(HttpMethod.DELETE, uriPath, null, response); | ||||||
| @@ -237,6 +238,10 @@ public abstract class UseCase<T extends UseCase<?>> { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private static Duration seconds(final int secondsIfNoDebuggerAttached) { | ||||||
|  |         return isDebuggerAttached() ? Duration.ofHours(1) : Duration.ofSeconds(secondsIfNoDebuggerAttached); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public final class HttpResponse { |     public final class HttpResponse { | ||||||
|  |  | ||||||
|         @Getter |         @Getter | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransacti | |||||||
|         requires("CoopAssets-Transaction with incorrect assetValue", alias -> |         requires("CoopAssets-Transaction with incorrect assetValue", alias -> | ||||||
|                 new CreateCoopAssetsDepositTransaction(testSuite) |                 new CreateCoopAssetsDepositTransaction(testSuite) | ||||||
|                         .given("memberNumber", "%{memberNumber}") |                         .given("memberNumber", "%{memberNumber}") | ||||||
|                         .given("reference", "sign %{dateOfIncorrectTransaction}") // same as revertedAssetTx |                         .given("reference", "sign %{dateOfIncorrectTransaction}") // same as relatedAssetTx | ||||||
|                         .given("assetValue", 10) |                         .given("assetValue", 10) | ||||||
|                         .given("comment", "coop-assets deposit transaction with wrong asset value") |                         .given("comment", "coop-assets deposit transaction with wrong asset value") | ||||||
|                         .given("transactionDate", "%{dateOfIncorrectTransaction}") |                         .given("transactionDate", "%{dateOfIncorrectTransaction}") | ||||||
| @@ -20,7 +20,7 @@ public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransacti | |||||||
|     @Override |     @Override | ||||||
|     protected HttpResponse run() { |     protected HttpResponse run() { | ||||||
|         given("transactionType", "REVERSAL"); |         given("transactionType", "REVERSAL"); | ||||||
|         given("assetValue", -100); |         given("assetValue", -10); | ||||||
|         given("revertedAssetTx", uuid("CoopAssets-Transaction with incorrect assetValue")); |         given("revertedAssetTx", uuid("CoopAssets-Transaction with incorrect assetValue")); | ||||||
|         return super.run(); |         return super.run(); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -32,7 +32,8 @@ public abstract class CreateCoopAssetsTransaction extends UseCase<CreateCoopAsse | |||||||
|                     "assetValue": ${assetValue}, |                     "assetValue": ${assetValue}, | ||||||
|                     "comment": ${comment}, |                     "comment": ${comment}, | ||||||
|                     "valueDate": ${transactionDate}, |                     "valueDate": ${transactionDate}, | ||||||
|                     "revertedAssetTx.uuid": ${revertedAssetTx???} |                     "revertedAssetTx.uuid": ${revertedAssetTx???}, | ||||||
|  |                     "adoptingMembership.memberNumber": ${adoptingMemberNumber???} | ||||||
|                 } |                 } | ||||||
|                 """)) |                 """)) | ||||||
|                 .expecting(HttpStatus.CREATED).expecting(ContentType.JSON) |                 .expecting(HttpStatus.CREATED).expecting(ContentType.JSON) | ||||||
|   | |||||||
| @@ -0,0 +1,46 @@ | |||||||
|  | package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets; | ||||||
|  |  | ||||||
|  | import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; | ||||||
|  | import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateMembership; | ||||||
|  | import net.hostsharing.hsadminng.hs.office.scenarios.partner.CreatePartner; | ||||||
|  |  | ||||||
|  | import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; | ||||||
|  |  | ||||||
|  | public class CreateCoopAssetsTransferTransaction extends CreateCoopAssetsTransaction { | ||||||
|  |  | ||||||
|  |     public CreateCoopAssetsTransferTransaction(final ScenarioTest testSuite) { | ||||||
|  |         super(testSuite); | ||||||
|  |  | ||||||
|  |         requires("Partner: New AG", alias -> new CreatePartner(testSuite, alias) | ||||||
|  |                 .given("partnerNumber", toPartnerNumber("%{adoptingMemberNumber}")) | ||||||
|  |                 .given("personType", "LEGAL_PERSON") | ||||||
|  |                 .given("tradeName", "New AG") | ||||||
|  |                 .given("contactCaption", "New AG - Board of Directors") | ||||||
|  |                 .given("emailAddress", "board-of-directors@new-ag.example.org") | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         requires("Membership: New AG", alias -> new CreateMembership(testSuite) | ||||||
|  |                 .given("partnerNumber", toPartnerNumber("%{adoptingMemberNumber}")) | ||||||
|  |                 .given("partnerName", "New AG") | ||||||
|  |                 .given("validFrom", "2024-11-15") | ||||||
|  |                 .given("newStatus", "ACTIVE") | ||||||
|  |                 .given("membershipFeeBillable", "true") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected HttpResponse run() { | ||||||
|  |         introduction("Additionally to the TRANSFER, the ADOPTION is automatically booked for the receiving member."); | ||||||
|  |  | ||||||
|  |         given("memberNumber", "%{transferringMemberNumber}"); | ||||||
|  |         given("transactionType", "TRANSFER"); | ||||||
|  |         given("assetValue", "-%{valueToDisburse}"); | ||||||
|  |         given("assetValue", "-%{valueToDisburse}"); | ||||||
|  |         return super.run(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private String toPartnerNumber(final String resolvableString) { | ||||||
|  |         final var memberNumber = ScenarioTest.resolve(resolvableString, DROP_COMMENTS); | ||||||
|  |         return "P-" + memberNumber.substring("M-".length(), 7); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | package net.hostsharing.hsadminng.test; | ||||||
|  |  | ||||||
|  | import lombok.experimental.UtilityClass; | ||||||
|  |  | ||||||
|  | import java.lang.management.ManagementFactory; | ||||||
|  |  | ||||||
|  | @UtilityClass | ||||||
|  | public class DebuggerDetection { | ||||||
|  |     public static boolean isDebuggerAttached() { | ||||||
|  |         // check for typical debug arguments in the JVM input arguments | ||||||
|  |         return ManagementFactory.getRuntimeMXBean().getInputArguments().stream() | ||||||
|  |                 .anyMatch(arg -> arg.contains("-agentlib:jdwp")); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | package net.hostsharing.hsadminng.test; | ||||||
|  |  | ||||||
|  | import java.lang.annotation.ElementType; | ||||||
|  | import java.lang.annotation.Retention; | ||||||
|  | import java.lang.annotation.RetentionPolicy; | ||||||
|  | import java.lang.annotation.Target; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Use this annotation on JUnit Jupiter test-methods to convert failure to ignore. | ||||||
|  |  * | ||||||
|  |  * <p> | ||||||
|  |  *     The test-class also has to add the extension {link IgnoreOnFailureExtension}. | ||||||
|  |  * </p> | ||||||
|  |  */ | ||||||
|  | @Target(ElementType.METHOD) | ||||||
|  | @Retention(RetentionPolicy.RUNTIME) | ||||||
|  | public @interface IgnoreOnFailure { | ||||||
|  |     ///  a comment, e.g. about the feature under construction | ||||||
|  |     String value() default ""; | ||||||
|  | } | ||||||
| @@ -0,0 +1,52 @@ | |||||||
|  | package net.hostsharing.hsadminng.test; | ||||||
|  |  | ||||||
|  | import org.junit.jupiter.api.extension.ExtensionContext; | ||||||
|  |  | ||||||
|  | import org.junit.jupiter.api.extension.InvocationInterceptor; | ||||||
|  | import org.junit.jupiter.api.extension.ReflectiveInvocationContext; | ||||||
|  |  | ||||||
|  | import java.lang.reflect.Method; | ||||||
|  |  | ||||||
|  | import static org.assertj.core.api.Assumptions.assumeThat; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Use this JUnit Jupiter extension to ignore failing tests annotated with annotation {@link IgnoreOnFailure}. | ||||||
|  |  * | ||||||
|  |  * <p> | ||||||
|  |  *     This is useful for outside-in-TDD, if you write a high-level (e.g. Acceptance- or Scenario-Test) before | ||||||
|  |  *     you even have an implementation for that new feature. | ||||||
|  |  *     As long as no other tests breaks, it's not a real problem merging your new test and incomplete implementation. | ||||||
|  |  * </p> | ||||||
|  |  * <p> | ||||||
|  |  *     Once the test turns green, remove the annotation  {@link IgnoreOnFailure}. | ||||||
|  |  * </p> | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | // BLOG: A JUnit Jupiter extension to ignore failed acceptance tests for outside-in TDD | ||||||
|  | public class IgnoreOnFailureExtension implements InvocationInterceptor { | ||||||
|  |  | ||||||
|  |     /// @hidden | ||||||
|  |     @Override | ||||||
|  |     public void interceptTestMethod( | ||||||
|  |             final Invocation<Void> invocation, | ||||||
|  |             final ReflectiveInvocationContext<Method> invocationContext, | ||||||
|  |             final ExtensionContext extensionContext) throws Throwable { | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             invocation.proceed(); | ||||||
|  |         } catch (final Throwable throwable) { | ||||||
|  |             if (hasIgnoreOnFailureAnnotation(extensionContext)) { | ||||||
|  |                 assumeThat(true).as("ignoring failed test with @" + IgnoreOnFailure.class.getSimpleName()).isFalse(); | ||||||
|  |             } else { | ||||||
|  |                 throw throwable; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static boolean hasIgnoreOnFailureAnnotation(final ExtensionContext context) { | ||||||
|  |         final var hasIgnoreOnFailureAnnotation = context.getTestMethod() | ||||||
|  |                 .map(method -> method.getAnnotation(IgnoreOnFailure.class)) | ||||||
|  |                 .isPresent(); | ||||||
|  |         return hasIgnoreOnFailureAnnotation; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,87 @@ | |||||||
|  | package net.hostsharing.hsadminng.test; | ||||||
|  |  | ||||||
|  | import lombok.experimental.UtilityClass; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  |  | ||||||
|  | import java.util.HashSet; | ||||||
|  | import java.util.LinkedList; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Queue; | ||||||
|  | import java.util.Set; | ||||||
|  | import java.util.UUID; | ||||||
|  |  | ||||||
|  | @UtilityClass | ||||||
|  | public class TestUuidGenerator { | ||||||
|  |  | ||||||
|  |     private static final UUID ZEROES_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); | ||||||
|  |  | ||||||
|  |     private static final List<UUID> GIVEN_UUIDS = List.of( | ||||||
|  |             ZEROES_UUID, | ||||||
|  |             uuidWithDigit(1), | ||||||
|  |             uuidWithDigit(2), | ||||||
|  |             uuidWithDigit(3), | ||||||
|  |             uuidWithDigit(4), | ||||||
|  |             uuidWithDigit(5), | ||||||
|  |             uuidWithDigit(6), | ||||||
|  |             uuidWithDigit(7), | ||||||
|  |             uuidWithDigit(8), | ||||||
|  |             uuidWithDigit(9), | ||||||
|  |             uuidWithChar('a'), | ||||||
|  |             uuidWithChar('b'), | ||||||
|  |             uuidWithChar('c'), | ||||||
|  |             uuidWithChar('d'), | ||||||
|  |             uuidWithChar('e'), | ||||||
|  |             uuidWithChar('f') | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |     private static Set<Integer> staticallyUsedIndexes = new HashSet<>(); | ||||||
|  |  | ||||||
|  |     private Queue<UUID> availableUuids = null; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     public static void start(final int firstIndex) { | ||||||
|  |         if (staticallyUsedIndexes.contains(firstIndex)) { | ||||||
|  |             throw new IllegalArgumentException(firstIndex + " already used statically, try higher and amend references"); | ||||||
|  |         } | ||||||
|  |         availableUuids = new LinkedList<>(GIVEN_UUIDS.subList(firstIndex, GIVEN_UUIDS.size())); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static UUID next() { | ||||||
|  |         if (availableUuids == null) { | ||||||
|  |             throw new IllegalStateException("UUID generator not started yet, call start() in @BeforeEach."); | ||||||
|  |         } | ||||||
|  |         if (availableUuids.isEmpty()) { | ||||||
|  |             throw new IllegalStateException("No UUIDs available anymore."); | ||||||
|  |         } | ||||||
|  |         return availableUuids.poll(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Marks the UUID as used in static initializers. | ||||||
|  |      * | ||||||
|  |      * @param index 0..15 | ||||||
|  |      * @return a constant UUID related to the given index | ||||||
|  |      */ | ||||||
|  |     public static UUID use(final int index) { | ||||||
|  |         staticallyUsedIndexes.add(index); | ||||||
|  |         return GIVEN_UUIDS.get(index); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * References the UUID from the given index. | ||||||
|  |      * | ||||||
|  |      * @param index 0..15 | ||||||
|  |      * @return a constant UUID related to the given index | ||||||
|  |      */ | ||||||
|  |     public static UUID ref(final int index) { | ||||||
|  |         return GIVEN_UUIDS.get(index); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static @NotNull UUID uuidWithDigit(final int digit) { | ||||||
|  |         return UUID.fromString(ZEROES_UUID.toString().replace('0', Character.forDigit(digit, 16))); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static @NotNull UUID uuidWithChar(final char hexDigit) { | ||||||
|  |         return UUID.fromString(ZEROES_UUID.toString().replace('0', hexDigit)); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user