diff --git a/README.md b/README.md index 308d5c51..38defd3b 100644 --- a/README.md +++ b/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, 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 and would not need the `rbac.role` table anymore. We would also reduce the depth of the expensive recursive CTE-query. @@ -591,6 +591,12 @@ 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.) +### 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 Configure .pgpass for the Default PostgreSQL Database? diff --git a/build.gradle b/build.gradle index 876ff878..fc287915 100644 --- a/build.gradle +++ b/build.gradle @@ -445,3 +445,8 @@ tasks.register('convertMarkdownToHtml') { } } convertMarkdownToHtml.dependsOn scenarioTests + +// shortcut for compiling all files +tasks.register('compile') { + dependsOn 'compileJava', 'compileTestJava' +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java index 06bec54e..1e7eeade 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java @@ -1,10 +1,15 @@ package net.hostsharing.hsadminng.hs.office.coopassets; 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.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.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; @@ -14,13 +19,18 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import jakarta.persistence.EntityNotFoundException; +import jakarta.validation.ValidationException; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; -import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.*; +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 public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi { @@ -29,11 +39,17 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse private Context context; @Autowired - private StandardMapper mapper; + private StrictMapper mapper; + + @Autowired + private EntityManagerWrapper emw; @Autowired private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo; + @Autowired + private HsOfficeMembershipRepository membershipRepo; + @Override @Transactional(readOnly = true) public ResponseEntity> getListOfCoopAssets( @@ -49,7 +65,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse fromValueDate, 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); } @@ -63,7 +79,10 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse context.define(currentSubject, assumedRoles); 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 uri = @@ -71,14 +90,14 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse .path("/api/hs/office/coopassetstransactions/{id}") .buildAndExpand(saved.getUuid()) .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); } @Override @Transactional(readOnly = true) public ResponseEntity getSingleCoopAssetTransactionByUuid( - final String currentSubject, final String assumedRoles, final UUID assetTransactionUuid) { + final String currentSubject, final String assumedRoles, final UUID assetTransactionUuid) { context.define(currentSubject, assumedRoles); @@ -101,7 +120,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse private static void validateDebitTransaction( final HsOfficeCoopAssetsTransactionInsertResource requestBody, final ArrayList violations) { - if (List.of(DEPOSIT, ADOPTION).contains(requestBody.getTransactionType()) + if (List.of(DEPOSIT, HsOfficeCoopAssetsTransactionTypeResource.ADOPTION).contains(requestBody.getTransactionType()) && requestBody.getAssetValue().signum() < 0) { violations.add("for %s, assetValue must be positive but is \"%.2f\"".formatted( requestBody.getTransactionType(), requestBody.getAssetValue())); @@ -127,10 +146,88 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse } } - final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { - if ( resource.getRevertedAssetTxUuid() != null ) { - entity.setRevertedAssetTx(coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid()) - .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] reverseEntityUuid %s not found".formatted(resource.getRevertedAssetTxUuid())))); + final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { + if (entity.getReversalAssetTx() != null) { + resource.getReversalAssetTx().setRevertedAssetTxUuid(entity.getUuid()); + } + if (entity.getRevertedAssetTx() != null) { + resource.getRevertedAssetTx().setReversalAssetTxUuid(entity.getUuid()); + } + if (entity.getAdoptionAssetTx() != null) { + resource.getAdoptionAssetTx().setTransferAssetTxUuid(entity.getUuid()); + } + if (entity.getTransferAssetTx() != null) { + resource.getTransferAssetTx().setAdoptionAssetTxUuid(entity.getUuid()); } }; -}; + + final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { + if (resource.getMembershipUuid() != null) { + entity.setMembership(emw.getReference(HsOfficeMembershipEntity.class, resource.getMembershipUuid())); + } + if (resource.getRevertedAssetTxUuid() != null) { + entity.setRevertedAssetTx(coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid()) + .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] revertedEntityUuid %s not found".formatted( + resource.getRevertedAssetTxUuid())))); + } + + 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(); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java index cc2e504b..395c2895 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java @@ -50,8 +50,10 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE .withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue) .withProp(HsOfficeCoopAssetsTransactionEntity::getReference) .withProp(HsOfficeCoopAssetsTransactionEntity::getComment) - .withProp(at -> ofNullable(at.getRevertedAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null)) - .withProp(at -> ofNullable(at.getReversalAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null)) + .withProp(HsOfficeCoopAssetsTransactionEntity::getRevertedAssetTx) + .withProp(HsOfficeCoopAssetsTransactionEntity::getReversalAssetTx) + .withProp(HsOfficeCoopAssetsTransactionEntity::getAdoptionAssetTx) + .withProp(HsOfficeCoopAssetsTransactionEntity::getTransferAssetTx) .quotedValues(false); @Id @@ -95,16 +97,24 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE @Column(name = "comment") private String comment; - /** - * Optionally, the UUID of the corresponding transaction for an reversal transaction. - */ - @OneToOne + // Optionally, the UUID of the corresponding transaction for a reversal transaction. + @OneToOne(cascade = CascadeType.PERSIST) // TODO.impl: can probably be removed after office data migration @JoinColumn(name = "revertedassettxuuid") private HsOfficeCoopAssetsTransactionEntity revertedAssetTx; + // and the other way around @OneToOne(mappedBy = "revertedAssetTx") 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 public HsOfficeCoopAssetsTransactionEntity load() { BaseEntity.super.load(); diff --git a/src/main/java/net/hostsharing/hsadminng/repr/Stringify.java b/src/main/java/net/hostsharing/hsadminng/repr/Stringify.java index aaa3ea73..f1e1c709 100644 --- a/src/main/java/net/hostsharing/hsadminng/repr/Stringify.java +++ b/src/main/java/net/hostsharing/hsadminng/repr/Stringify.java @@ -80,12 +80,6 @@ public final class Stringify { .map(prop -> PropertyValue.of(prop, prop.getter.apply(object))) .filter(Objects::nonNull) .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)) .collect(Collectors.joining(separator)); return idProp != null @@ -131,7 +125,11 @@ public final class Stringify { private record PropertyValue(Property prop, Object rawValue, String value) { static PropertyValue of(Property 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() { diff --git a/src/main/resources/api-definition/hs-office/hs-office-coopassets-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-coopassets-schemas.yaml index 30725a82..4474b221 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-coopassets-schemas.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-coopassets-schemas.yaml @@ -32,15 +32,22 @@ components: type: string comment: 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: - $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: - $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: - Similar to `HsOfficeCoopAssetsTransaction` but without the self-referencing properties - (`revertedAssetTx` and `reversalAssetTx`), to avoid recursive JSON. + Similar to `HsOfficeCoopAssetsTransaction` but just the UUID of the related property, to avoid recursive JSON. type: object properties: uuid: @@ -58,6 +65,22 @@ components: type: string comment: 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: type: object @@ -83,6 +106,14 @@ components: revertedAssetTx.uuid: type: string format: uuid + adoptingMembership.uuid: + type: string + format: uuid + adoptingMembership.memberNumber: + type: string + minLength: 9 + maxLength: 9 + pattern: 'M-[0-9]{7}' required: - membership.uuid - transactionType diff --git a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql index 8feac5da..ce076885 100644 --- a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql +++ b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql @@ -24,7 +24,8 @@ create table if not exists hs_office.coopassettx valueDate date not null, assetValue numeric(12,2) not null, -- https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_money 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) ); --// @@ -35,9 +36,20 @@ create table if not exists hs_office.coopassettx -- ---------------------------------------------------------------------------- alter table hs_office.coopassettx - add constraint reverse_entry_missing - check ( transactionType = 'REVERSAL' and revertedAssetTxUuid is not null - or transactionType <> 'REVERSAL' and revertedAssetTxUuid is null); + add constraint reversal_asset_tx_must_have_reverted_asset_tx + check (transactionType <> 'REVERSAL' or revertedAssetTxuUid is not 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); --// -- ============================================================================ diff --git a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5128-hs-office-coopassets-test-data.sql b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5128-hs-office-coopassets-test-data.sql index ac43a731..85206b83 100644 --- a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5128-hs-office-coopassets-test-data.sql +++ b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5128-hs-office-coopassets-test-data.sql @@ -15,7 +15,9 @@ create or replace procedure hs_office.coopassettx_create_test_data( language plpgsql as $$ declare membership hs_office.membership; - lossEntryUuid uuid; + invalidLossTx uuid; + transferTx uuid; + adoptionTx uuid; begin select m.uuid from hs_office.membership m @@ -25,14 +27,18 @@ begin into membership; raise notice 'creating test coopAssetsTransaction: %', givenPartnerNumber || givenMemberNumberSuffix; - lossEntryUuid := uuid_generate_v4(); + invalidLossTx := uuid_generate_v4(); + transferTx := uuid_generate_v4(); + adoptionTx := uuid_generate_v4(); 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 - (uuid_generate_v4(), membership.uuid, 'DEPOSIT', '2010-03-15', 320.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-1', 'initial deposit', null), - (uuid_generate_v4(), membership.uuid, 'DISBURSAL', '2021-09-01', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-2', 'partial disbursal', null), - (lossEntryUuid, membership.uuid, 'DEPOSIT', '2022-10-20', 128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some loss', null), - (uuid_generate_v4(), membership.uuid, 'REVERSAL', '2022-10-21', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some reversal', lossEntryUuid); + (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, 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', 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; $$; --// diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java index 0bdf47da..8d3682c9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java @@ -436,7 +436,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport { 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), 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), 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), @@ -864,21 +864,44 @@ public abstract class BaseOfficeDataImport extends CsvDataImport { .comment(rec.getString("comment")) .reference(member.getMemberNumber().toString()) .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.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) { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/CsvDataImport.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/CsvDataImport.java index d04fc0a5..311302c7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/CsvDataImport.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/CsvDataImport.java @@ -172,10 +172,14 @@ public class CsvDataImport extends ContextBasedTest { public T persistViaEM(final Integer id, final T entity) { //System.out.println("persisting #" + entity.hashCode() + ": " + entity); em.persist(entity); - // uncomment for debugging purposes - // em.flush(); // makes it slow, but produces better error messages - // System.out.println("persisted #" + entity.hashCode() + " as " + entity.getUuid()); - return entity; + // uncomment for debugging purposes FIXME + try { + 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 + } } @SneakyThrows diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerRestTest.java index 624e9994..f17a18a7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerRestTest.java @@ -25,6 +25,7 @@ class HsOfficeBankAccountControllerRestTest { Context contextMock; @MockBean + @SuppressWarnings("unused") // not used in test, but in controller class StandardMapper mapper; @MockBean diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java index d5785ffa..bc986db0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java @@ -69,7 +69,7 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased .then().log().all().assertThat() .statusCode(200) .contentType("application/json") - .body("", hasSize(12)); // @formatter:on + .body("", hasSize(3*6)); // @formatter:on } @Test @@ -94,14 +94,22 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased "assetValue": 320.00, "valueDate": "2010-03-15", "reference": "ref 1000202-1", - "comment": "initial deposit" + "comment": "initial deposit", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": null, + "reversalAssetTx": null }, { "transactionType": "DISBURSAL", "assetValue": -128.00, "valueDate": "2021-09-01", "reference": "ref 1000202-2", - "comment": "partial disbursal" + "comment": "partial disbursal", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": null, + "reversalAssetTx": null }, { "transactionType": "DEPOSIT", @@ -109,12 +117,18 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased "valueDate": "2022-10-20", "reference": "ref 1000202-3", "comment": "some loss", + "adoptionAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": null, "reversalAssetTx": { "transactionType": "REVERSAL", "assetValue": -128.00, "valueDate": "2022-10-21", "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", "reference": "ref 1000202-3", "comment": "some reversal", + "adoptionAssetTx": null, + "transferAssetTx": null, "revertedAssetTx": { "transactionType": "DEPOSIT", "assetValue": 128.00, "valueDate": "2022-10-20", "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 diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java index 46779410..dff6f356 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerRestTest.java @@ -1,40 +1,75 @@ package net.hostsharing.hsadminng.hs.office.coopassets; +import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration; 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 org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; 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.request.MockMvcRequestBuilders; +import java.util.Optional; import java.util.UUID; import java.util.function.Function; 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.mockito.ArgumentMatchers.any; +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.status; @WebMvcTest(HsOfficeCoopAssetsTransactionController.class) +@Import({ StrictMapper.class, JsonObjectMapperConfiguration.class }) +@RunWith(SpringRunner.class) class HsOfficeCoopAssetsTransactionControllerRestTest { + private static final UUID AVAILABLE_MEMBERSHIP_UUID = UUID.randomUUID(); + public static final HsOfficeMembershipEntity AVAILABLE_MEMBER_ENTITY = HsOfficeMembershipEntity.builder() + .uuid(AVAILABLE_MEMBERSHIP_UUID) + .partner(HsOfficePartnerEntity.builder() + .partnerNumber(12345) + .build()) + .memberNumberSuffix("00") + .build(); + private static final UUID UNAVAILABLE_MEMBERSHIP_UUID = UUID.randomUUID(); + private static final String UNAVAILABLE_MEMBER_NUMBER = "M-1234699"; + private static final String AVAILABLE_MEMBER_NUMBER = "M-1234600"; + @Autowired MockMvc mockMvc; @MockBean Context contextMock; + @Autowired + @SuppressWarnings("unused") // not used in test, but in controller class + StrictMapper mapper; + @MockBean - StandardMapper mapper; + @SuppressWarnings("unused") // not used in test, but in base-class of StrictMapper + EntityManagerWrapper em; @MockBean HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo; + @MockBean + HsOfficeMembershipRepository membershipRepo; + static final String VALID_INSERT_REQUEST_BODY = """ { "membership.uuid": "%s", @@ -42,7 +77,9 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { "assetValue": 128.00, "valueDate": "2022-10-13", "reference": "valid reference", - "comment": "valid comment" + "comment": "valid comment", + "adoptingMembership.uuid": null, + "adoptingMembership.memberNumber": null } """.formatted(UUID.randomUUID()); @@ -65,8 +102,6 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { .with("assetValue", -64.00), "[for DEPOSIT, assetValue must be positive but is \"-64.00\"]"), - //TODO: other transaction types - ASSETS_VALUE_FOR_DISBURSAL_MUST_BE_NEGATIVE( requestBody -> requestBody .with("transactionType", "DISBURSAL") @@ -75,6 +110,20 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { //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( requestBody -> requestBody .with("transactionType", "REVERSAL") @@ -111,6 +160,7 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { @ParameterizedTest @EnumSource(BadRequestTestCases.class) void respondWithBadRequest(final BadRequestTestCases testCase) throws Exception { +// assumeThat(testCase == ADOPTING_MEMBERSHIP_NUMBER_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE).isTrue(); // when mockMvc.perform(MockMvcRequestBuilders @@ -127,4 +177,91 @@ class HsOfficeCoopAssetsTransactionControllerRestTest { .andExpect(status().is4xxClientError()); } + enum SuccessfullyCreatedTestCases { + + ADOPTING_MEMBERSHIP_NUMBER_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE( + requestBody -> requestBody + .with("transactionType", "TRANSFER") + .with("assetValue", -64.00) + .with("adoptingMembership.memberNumber", AVAILABLE_MEMBER_NUMBER), + """ + { + "transactionType": "TRANSFER", + "assetValue": -64.00, + "adoptionAssetTx": { + "transactionType": "ADOPTION", + "assetValue": 64.00 + }, + "reversalAssetTx": null, + "transferAssetTx": null, + "revertedAssetTx": null + } + """), + + ADOPTING_MEMBERSHIP_UUID_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE( + requestBody -> requestBody + .with("transactionType", "TRANSFER") + .with("assetValue", -64.00) + .with("adoptingMembership.uuid", AVAILABLE_MEMBERSHIP_UUID.toString()), + """ + { + "transactionType": "TRANSFER", + "assetValue": -64.00, + "adoptionAssetTx": { + "transactionType": "ADOPTION", + "assetValue": 64.00 + }, + "transferAssetTx": null, + "revertedAssetTx": null, + "reversalAssetTx": null + } + """); + + private final Function givenBodyTransformation; + private final String expectedResponseBody; + + SuccessfullyCreatedTestCases( + final Function givenBodyTransformation, + final String expectedResponseBody) { + this.givenBodyTransformation = givenBodyTransformation; + this.expectedResponseBody = expectedResponseBody; + } + + String givenRequestBody() { + return givenBodyTransformation.apply(jsonObject(VALID_INSERT_REQUEST_BODY)).toString(); + } + } + + @ParameterizedTest + @EnumSource(SuccessfullyCreatedTestCases.class) + void respondWithSuccessfullyCreated(final SuccessfullyCreatedTestCases testCase) throws Exception { + + // 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(jsonPath("$", lenientlyEquals(testCase.expectedResponseBody))) + .andExpect(status().is2xxSuccessful()); + } + + @BeforeEach + void initMocks() { + final var availableMemberNumber = Integer.valueOf(AVAILABLE_MEMBER_NUMBER.substring("M-".length())); + when(membershipRepo.findMembershipByMemberNumber(availableMemberNumber)).thenReturn(AVAILABLE_MEMBER_ENTITY); + when(membershipRepo.findByUuid(AVAILABLE_MEMBERSHIP_UUID)).thenReturn(Optional.of(AVAILABLE_MEMBER_ENTITY)); + when(coopAssetsTransactionRepo.save(any(HsOfficeCoopAssetsTransactionEntity.class))) + .thenAnswer(invocation -> { + final var entity = (HsOfficeCoopAssetsTransactionEntity) invocation.getArgument(0); + if (entity.getUuid() == null) { + entity.setUuid(UUID.randomUUID()); + } + return entity; + } + ); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntityUnitTest.java index 96a0edfb..cc6d2122 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntityUnitTest.java @@ -20,7 +20,6 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest { .comment("some comment") .build(); - final HsOfficeCoopAssetsTransactionEntity givenCoopAssetReversalTransaction = HsOfficeCoopAssetsTransactionEntity.builder() .membership(TEST_MEMBERSHIP) .reference("some-ref") @@ -31,6 +30,16 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest { .revertedAssetTx(givenCoopAssetTransaction) .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(); @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)"); } + @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 void toShortStringContainsOnlyMemberNumberSuffixAndSharesCountOnly() { final var result = givenCoopAssetTransaction.toShortString(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java index 4c2f866d..40f9d0a7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java @@ -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: 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: 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: 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-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: 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-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 @@ -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: 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-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 @@ -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: 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-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)"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java index 821e8871..6da7ddd1 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java @@ -30,6 +30,7 @@ class HsOfficeCoopSharesTransactionControllerRestTest { Context contextMock; @MockBean + @SuppressWarnings("unused") // not used in test, but in controller class StandardMapper mapper; @MockBean diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityUnitTest.java index 6394a8d7..bef9af31 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityUnitTest.java @@ -40,7 +40,7 @@ class HsOfficeCoopSharesTransactionEntityUnitTest { } @Test - void toStringWithRevertedAssetTxContainsRevertedAssetTx() { + void toStringWithRelatedAssetTxContainsRelatedAssetTx() { givenCoopSharesTransaction.setRevertedShareTx(givenCoopShareReversalTransaction); final var result = givenCoopSharesTransaction.toString(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java index 0f7362df..7adc6805 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java @@ -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.CreateCoopAssetsDisbursalTransaction; 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.CreateCoopSharesRevertTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesSubscriptionTransaction; @@ -77,8 +78,8 @@ class HsOfficeScenarioTests extends ScenarioTest { @Test @Order(1011) - @Produces(explicitly = "Partner: P-31011 - Michelle Matthieu", implicitly = { "Person: Michelle Matthieu", - "Contact: Michelle Matthieu" }) + @Produces(explicitly = "Partner: P-31011 - Michelle Matthieu", + implicitly = { "Person: Michelle Matthieu", "Contact: Michelle Matthieu" }) void shouldCreateNaturalPersonAsPartner() { new CreatePartner(this) .given("partnerNumber", "P-31011") @@ -336,7 +337,7 @@ class HsOfficeScenarioTests extends ScenarioTest { @Test @Order(4201) @Requires("Membership: M-3101000 - Test AG") - @Produces("Coop-Shares SUBSCRIPTION Transaction") + @Produces("Coop-Shares M-3101000 - Test AG - SUBSCRIPTION Transaction") void shouldSubscribeCoopShares() { new CreateCoopSharesSubscriptionTransaction(this) .given("memberNumber", "M-3101000") @@ -360,8 +361,8 @@ class HsOfficeScenarioTests extends ScenarioTest { @Test @Order(4202) - @Requires("Coop-Shares SUBSCRIPTION Transaction") - @Produces("Coop-Shares CANCELLATION Transaction") + @Requires("Coop-Shares M-3101000 - Test AG - SUBSCRIPTION Transaction") + @Produces("Coop-Shares M-3101000 - Test AG - CANCELLATION Transaction") void shouldCancelCoopSharesSubscription() { new CreateCoopSharesCancellationTransaction(this) .given("memberNumber", "M-3101000") @@ -375,7 +376,7 @@ class HsOfficeScenarioTests extends ScenarioTest { @Test @Order(4301) @Requires("Membership: M-3101000 - Test AG") - @Produces("Coop-Assets DEPOSIT Transaction") + @Produces("Coop-Assets M-3101000 - Test AG - DEPOSIT Transaction") void shouldSubscribeCoopAssets() { new CreateCoopAssetsDepositTransaction(this) .given("memberNumber", "M-3101000") @@ -388,7 +389,7 @@ class HsOfficeScenarioTests extends ScenarioTest { @Test @Order(4302) - @Requires("Coop-Assets DEPOSIT Transaction") + @Requires("Membership: M-3101000 - Test AG") void shouldRevertCoopAssetsSubscription() { new CreateCoopAssetsRevertTransaction(this) .given("memberNumber", "M-3101000") @@ -399,8 +400,8 @@ class HsOfficeScenarioTests extends ScenarioTest { @Test @Order(4302) - @Requires("Coop-Assets DEPOSIT Transaction") - @Produces("Coop-Assets DISBURSAL Transaction") + @Requires("Coop-Assets M-3101000 - Test AG - DEPOSIT Transaction") + @Produces("Coop-Assets M-3101000 - Test AG - DISBURSAL Transaction") void shouldDisburseCoopAssets() { new CreateCoopAssetsDisbursalTransaction(this) .given("memberNumber", "M-3101000") @@ -411,6 +412,23 @@ class HsOfficeScenarioTests extends ScenarioTest { .doRun(); } + @Test + @Order(4303) + @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-01-15") + .given("valueToDisburse", 2 * 64) + .given("comment", "transfer assets from M-3101000 to M-4303000") + .given("transactionDate", "2024-02-15") + .doRun(); + } + + // FIXME: implement revert for an asset TRANSFER tx + @Test @Order(4900) @Requires("Membership: M-3101000 - Test AG") diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransaction.java index a83c21a2..60d85fbe 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsRevertTransaction.java @@ -10,7 +10,7 @@ public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransacti requires("CoopAssets-Transaction with incorrect assetValue", alias -> new CreateCoopAssetsDepositTransaction(testSuite) .given("memberNumber", "%{memberNumber}") - .given("reference", "sign %{dateOfIncorrectTransaction}") // same as revertedAssetTx + .given("reference", "sign %{dateOfIncorrectTransaction}") // same as relatedAssetTx .given("assetValue", 10) .given("comment", "coop-assets deposit transaction with wrong asset value") .given("transactionDate", "%{dateOfIncorrectTransaction}") @@ -20,7 +20,7 @@ public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransacti @Override protected HttpResponse run() { given("transactionType", "REVERSAL"); - given("assetValue", -100); + given("assetValue", -10); given("revertedAssetTx", uuid("CoopAssets-Transaction with incorrect assetValue")); return super.run(); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransaction.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransaction.java index 56bc2a55..fbe4b8d5 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransaction.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/membership/coopassets/CreateCoopAssetsTransaction.java @@ -32,7 +32,8 @@ public abstract class CreateCoopAssetsTransaction extends UseCase 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); + } +}