Compare commits

..

16 Commits

Author SHA1 Message Date
Michael Hoennig
865ea3657e revert test 2024-11-22 17:21:36 +01:00
Michael Hoennig
d6d24803cd refactoring 2024-11-22 15:23:20 +01:00
Michael Hoennig
071c61a568 add membership.uuid and .memberNumber to HsOfficeCoopAssetsTransaction and HsOfficeRelatedCoopAssetsTransaction 2024-11-22 09:58:18 +01:00
Michael Hoennig
31d5786c0a add grep HOWTO to README.md 2024-11-21 13:03:30 +01:00
Michael Hoennig
defaa8665c scenario-Test shouldRevertCoopAssetsTransfer with IgnoreOnFailure 2024-11-21 12:39:26 +01:00
Michael Hoennig
bdbee6a158 1h timeout on REST-call if running in debugger 2024-11-21 12:38:35 +01:00
Michael Hoennig
b5b7f157ba testing-Tools DebuggerDetection and @IgnoreOnFailure with IgnoreOnFailureExtension 2024-11-21 12:37:59 +01:00
Michael Hoennig
2c3ce58da9 gradle wrapper now works without unbuffer 2024-11-21 12:37:29 +01:00
Michael Hoennig
d3237c819e cleanup debugging FIXME 2024-11-20 17:24:53 +01:00
Michael Hoennig
e6b1699210 add transfer+adoption asset tx to test-data 2024-11-20 17:23:43 +01:00
Michael Hoennig
87dfec9efe verify response body 2024-11-20 16:55:26 +01:00
Michael Hoennig
12837212ca add problem with Too Many Business-Rules Implemented in Controllers to README 2024-11-20 15:24:40 +01:00
Michael Hoennig
80b2d3fa8a add mappings to CoopAssetsTxController 2024-11-20 15:24:07 +01:00
Michael Hoennig
e89833b233 refactor Stringify 2024-11-19 17:40:51 +01:00
Michael Hoennig
e20c6faac5 fix REST+ImportTests 2024-11-19 17:26:37 +01:00
Michael Hoennig
566b6082df add TRANSFER->ADOPTION for coop assets to backend 2024-11-19 14:39:52 +01:00
40 changed files with 1095 additions and 455 deletions

View File

@ -8,12 +8,20 @@ gradleWrapper () {
return 1
fi
if command -v unbuffer >/dev/null 2>&1; then
# 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
}
postgresAutodoc () {

View File

@ -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,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.)
### 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 ...
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?
To access the default database schema as used during development, add this line to your `.pgpass` file in your users home directory:

View File

@ -445,3 +445,8 @@ tasks.register('convertMarkdownToHtml') {
}
}
convertMarkdownToHtml.dependsOn scenarioTests
// shortcut for compiling all files
tasks.register('compile') {
dependsOn 'compileJava', 'compileTestJava'
}

View File

@ -79,26 +79,12 @@ der Person des VIP-Contact (_Holder_) zur repräsentierten Person (_Anchor_) dar
### Operations-Contact
Ein _Operations-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner technischer Ansprechpartner ist.
Ein Seiteneffekt ist, dass diese Person im Ticketsystem Znuny direkt dem Geschäftspartner zugeordnet werden kann.
Im Legacy System waren das die Kontakte mit der Rolle `operation` und `silent`.
Ein _Operations-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner technischer Ansprechpartner ist
Implementiert ist der _Operations-Contact_ als eine besondere Form der [Relation](#Relation)
der Person des _Operations-Contact_ (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
### OperationsAlert-Contact
Ein _OperationsAlert-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner bei technischen Probleme kontaktiert werden soll.
Im Legacy System waren das die Kontakte mit der Rolle `operation`.
Implementiert ist der _OperationsAlert-Contact_ als eine besondere Form der [Relation](#Relation)
der Person des _OperationsAlert-Contact_ (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
### Subscriber-Contact
Ein _Subscriber-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner eine bestimmte Mailingliste abonniert.

View File

@ -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,19 @@ 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 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
public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi {
@ -29,11 +40,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<List<HsOfficeCoopAssetsTransactionResource>> getListOfCoopAssets(
@ -49,7 +66,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 +80,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,7 +91,7 @@ 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);
}
@ -101,7 +121,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
private static void validateDebitTransaction(
final HsOfficeCoopAssetsTransactionInsertResource requestBody,
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) {
violations.add("for %s, assetValue must be positive but is \"%.2f\"".formatted(
requestBody.getTransactionType(), requestBody.getAssetValue()));
@ -127,10 +147,111 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
}
}
final BiConsumer<HsOfficeCoopAssetsTransactionInsertResource, HsOfficeCoopAssetsTransactionEntity> 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<HsOfficeCoopAssetsTransactionEntity, HsOfficeCoopAssetsTransactionResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setMembershipUuid(entity.getMembership().getUuid());
resource.setMembershipMemberNumber(entity.getMembership().getTaggedMemberNumber());
if (entity.getReversalAssetTx() != null) {
resource.getReversalAssetTx().setRevertedAssetTxUuid(entity.getUuid());
resource.getReversalAssetTx().setMembershipUuid(entity.getMembership().getUuid());
resource.getReversalAssetTx().setMembershipMemberNumber(entity.getTaggedMemberNumber());
}
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();
}
}

View File

@ -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();

View File

@ -8,6 +8,5 @@ public enum HsOfficeRelationType {
VIP_CONTACT,
DEBITOR,
OPERATIONS,
OPERATIONS_ALERT,
SUBSCRIBER
}

View File

@ -80,12 +80,6 @@ public final class Stringify<B> {
.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<B> {
private record PropertyValue<B>(Property<B> prop, Object rawValue, String value) {
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() {

View File

@ -20,6 +20,15 @@ components:
uuid:
type: string
format: uuid
membership.uuid:
type: string
format: uuid
nullable: false
membership.memberNumber:
type: string
minLength: 9
maxLength: 9
pattern: 'M-[0-9]{7}'
transactionType:
$ref: '#/components/schemas/HsOfficeCoopAssetsTransactionType'
assetValue:
@ -32,20 +41,36 @@ 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:
type: string
format: uuid
membership.uuid:
type: string
format: uuid
nullable: false
membership.memberNumber:
type: string
minLength: 9
maxLength: 9
pattern: 'M-[0-9]{7}'
transactionType:
$ref: '#/components/schemas/HsOfficeCoopAssetsTransactionType'
assetValue:
@ -58,6 +83,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 +124,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

View File

@ -16,6 +16,10 @@ components:
uuid:
type: string
format: uuid
membership.uuid:
type: string
format: uuid
nullable: false
transactionType:
$ref: '#/components/schemas/HsOfficeCoopSharesTransactionType'
shareCount:
@ -41,6 +45,10 @@ components:
uuid:
type: string
format: uuid
membership.uuid:
type: string
format: uuid
nullable: false
transactionType:
$ref: '#/components/schemas/HsOfficeCoopSharesTransactionType'
shareCount:

View File

@ -13,7 +13,6 @@ components:
- REPRESENTATIVE
- VIP_CONTACT
- OPERATIONS
- OPERATIONS_ALERT
- SUBSCRIBER
HsOfficeRelation:

View File

@ -223,7 +223,7 @@ begin
)
select target.*
from %1$s as target
where target.uuid in (select * from accessible_uuids)
where rbac.isGlobalAdmin() or target.uuid in (select * from accessible_uuids)
order by %2$s;
grant all privileges on %1$s_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};

View File

@ -12,7 +12,6 @@ CREATE TYPE hs_office.RelationType AS ENUM (
'DEBITOR',
'VIP_CONTACT',
'OPERATIONS',
'OPERATIONS_ALERT',
'SUBSCRIBER');
CREATE CAST (character varying as hs_office.RelationType) WITH INOUT AS IMPLICIT;

View File

@ -7,7 +7,7 @@
--changeset michael.hoennig:hs-office-coopshares-MIGRATION-mapping endDelimiter:--//
-- ----------------------------------------------------------------------------
CREATE TABLE hs_office.coopsharetx_legacy_id
CREATE TABLE hs_office.coopsharestransaction_legacy_id
(
uuid uuid NOT NULL REFERENCES hs_office.coopsharetx(uuid),
member_share_id integer NOT NULL
@ -19,10 +19,10 @@ CREATE TABLE hs_office.coopsharetx_legacy_id
--changeset michael.hoennig:hs-office-coopshares-MIGRATION-sequence endDelimiter:--//
-- ----------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS hs_office.coopsharetx_legacy_id_seq
CREATE SEQUENCE IF NOT EXISTS hs_office.coopsharestransaction_legacy_id_seq
AS integer
START 1000000000
OWNED BY hs_office.coopsharetx_legacy_id.member_share_id;
OWNED BY hs_office.coopsharestransaction_legacy_id.member_share_id;
--//
@ -30,9 +30,9 @@ CREATE SEQUENCE IF NOT EXISTS hs_office.coopsharetx_legacy_id_seq
--changeset michael.hoennig:hs-office-coopshares-MIGRATION-default endDelimiter:--//
-- ----------------------------------------------------------------------------
ALTER TABLE hs_office.coopsharetx_legacy_id
ALTER TABLE hs_office.coopsharestransaction_legacy_id
ALTER COLUMN member_share_id
SET DEFAULT nextVal('hs_office.coopsharetx_legacy_id_seq');
SET DEFAULT nextVal('hs_office.coopsharestransaction_legacy_id_seq');
--/
@ -41,8 +41,8 @@ ALTER TABLE hs_office.coopsharetx_legacy_id
-- ----------------------------------------------------------------------------
CALL base.defineContext('schema-migration');
INSERT INTO hs_office.coopsharetx_legacy_id(uuid, member_share_id)
SELECT uuid, nextVal('hs_office.coopsharetx_legacy_id_seq') FROM hs_office.coopsharetx;
INSERT INTO hs_office.coopsharestransaction_legacy_id(uuid, member_share_id)
SELECT uuid, nextVal('hs_office.coopsharestransaction_legacy_id_seq') FROM hs_office.coopsharetx;
--/
@ -58,8 +58,8 @@ begin
raise exception 'invalid usage of trigger';
end if;
INSERT INTO hs_office.coopsharetx_legacy_id VALUES
(NEW.uuid, nextVal('hs_office.coopsharetx_legacy_id_seq'));
INSERT INTO hs_office.coopsharestransaction_legacy_id VALUES
(NEW.uuid, nextVal('hs_office.coopsharestransaction_legacy_id_seq'));
return NEW;
end; $$;
@ -83,7 +83,7 @@ begin
raise exception 'invalid usage of trigger';
end if;
DELETE FROM hs_office.coopsharetx_legacy_id
DELETE FROM hs_office.coopsharestransaction_legacy_id
WHERE uuid = OLD.uuid;
return OLD;

View File

@ -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);
--//
-- ============================================================================

View File

@ -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; $$;
--//

View File

@ -1,8 +0,0 @@
--liquibase formatted sql
-- ============================================================================
--changeset timotheus.pokorra:hs-integration-SCHEMA endDelimiter:--//
-- ----------------------------------------------------------------------------
CREATE SCHEMA hs_integration;
--//

View File

@ -1,18 +0,0 @@
--liquibase formatted sql
-- ============================================================================
--changeset timotheus.pokorra:hs-global-integration-kimai endDelimiter:--//
-- TODO.impl: also select column debitorNumber and do not filter anymore for '00'
CREATE OR REPLACE VIEW hs_integration.time_customer AS
SELECT p.partnernumber, debitor.defaultprefix
FROM hs_office.partner p
JOIN hs_office.relation AS pRel
ON pRel.type = 'PARTNER'
AND pRel.uuid = p.partnerRelUuid
JOIN hs_office.relation AS dRel
ON dRel.type = 'DEBITOR'
AND dRel.anchorUuid = pRel.holderUuid
JOIN hs_office.debitor AS debitor
ON debitor.debitorreluuid = dRel.uuid
AND debitor.debitornumbersuffix = '00';
--//

View File

@ -1,102 +0,0 @@
--liquibase formatted sql
-- ============================================================================
--changeset timotheus.pokorra:hs-global-integration-znuny endDelimiter:--//
-- TODO.impl: also select column debitorNumber and do not filter anymore for '00'
CREATE OR REPLACE VIEW hs_integration.contact AS
SELECT DISTINCT ON (uuid)
partner.partnernumber as partnernumber,
debitor.defaultprefix as defaultprefix,
c.uuid as uuid,
(CASE WHEN per.salutation <> '' THEN per.salutation ELSE NULL END) as salutation,
(CASE WHEN per.givenname <> '' THEN per.givenname ELSE NULL END) as givenname,
(CASE WHEN per.familyname <> '' THEN per.familyname ELSE NULL END) as familyname,
(CASE WHEN per.title <> '' THEN per.title ELSE NULL END) as title,
(CASE WHEN per.tradename <> '' THEN per.tradename ELSE NULL END) as tradename,
(CASE WHEN c.postaladdress->>'co' <> '' THEN c.postaladdress->>'co' ELSE NULL END) as co,
c.postaladdress->>'street' as street,
c.postaladdress->>'zipcode' as zipcode,
c.postaladdress->>'city' as city,
c.postaladdress->>'country' as country,
c.phonenumbers->>'phone_private' as phone_private,
c.phonenumbers->>'phone_office' as phone_office,
c.phonenumbers->>'phone_mobile' as phone_mobile,
c.phonenumbers->>'fax' as fax,
c.emailaddresses->>'main' as email
FROM hs_office.partner AS partner
JOIN hs_office.partner_legacy_id AS partner_lid ON partner_lid.uuid = partner.uuid
JOIN hs_office.relation AS pRel
ON pRel.type = 'PARTNER'
AND pRel.uuid = partner.partnerRelUuid
JOIN hs_office.relation AS dRel
ON dRel.type = 'DEBITOR'
AND dRel.anchorUuid = pRel.holderUuid
JOIN hs_office.debitor AS debitor
ON debitor.debitorreluuid = dRel.uuid
AND debitor.debitornumbersuffix = '00'
JOIN hs_office.contact AS c ON c.uuid = pRel.contactuuid
JOIN hs_office.person AS per ON per.uuid = pRel.holderuuid
UNION
SELECT DISTINCT ON (uuid)
partner.partnernumber as partnernumber,
debitor.defaultprefix as defaultprefix,
c.uuid as uuid,
(CASE WHEN per.salutation <> '' THEN per.salutation ELSE NULL END) as salutation,
(CASE WHEN per.givenname <> '' THEN per.givenname ELSE NULL END) as givenname,
(CASE WHEN per.familyname <> '' THEN per.familyname ELSE NULL END) as familyname,
(CASE WHEN per.title <> '' THEN per.title ELSE NULL END) as title,
(CASE WHEN per.tradename <> '' THEN per.tradename ELSE NULL END) as tradename,
(CASE WHEN c.postaladdress->>'co' <> '' THEN c.postaladdress->>'co' ELSE NULL END) as co,
c.postaladdress->>'street' as street,
c.postaladdress->>'zipcode' as zipcode,
c.postaladdress->>'city' as city,
c.postaladdress->>'country' as country,
c.phonenumbers->>'phone_private' as phone_private,
c.phonenumbers->>'phone_office' as phone_office,
c.phonenumbers->>'phone_mobile' as phone_mobile,
c.phonenumbers->>'fax' as fax,
c.emailaddresses->>'main' as email
FROM hs_office.partner AS partner
JOIN hs_office.relation AS pRel
ON pRel.type = 'PARTNER'
AND pRel.uuid = partner.partnerRelUuid
JOIN hs_office.relation AS dRel
ON dRel.type = 'DEBITOR'
AND dRel.anchorUuid = pRel.holderUuid
JOIN hs_office.debitor AS debitor
ON debitor.debitorreluuid = dRel.uuid
AND debitor.debitornumbersuffix = '00'
JOIN hs_office.relation AS rs1 ON rs1.uuid = partner.partnerreluuid AND rs1.type = 'PARTNER'
JOIN hs_office.relation AS relation ON relation.anchoruuid = rs1.holderuuid
JOIN hs_office.contact AS c ON c.uuid = relation.contactuuid
JOIN hs_office.person AS per ON per.uuid = relation.holderuuid;
CREATE OR REPLACE VIEW hs_integration.ticket_customer_user AS
SELECT c.uuid,
max(c.partnernumber)::text as number,
max(c.defaultprefix) as code,
max(c.email) as login,
max(c.salutation) as salut,
max(c.givenname) as firstname,
max(c.familyname) as lastname,
max(c.title) as title,
max(c.tradename) as firma,
max(c.co) as co,
max(c.street) as street,
max(c.zipcode) as zipcode,
max(c.city) as city,
max(c.country) as country,
max(concat_ws(', '::text, c.phone_office, c.phone_private)) AS phone,
max(c.phone_private) as phone_private,
max(c.phone_office) as phone_office,
max(c.phone_mobile) as mobile,
max(c.fax) as fax,
max(c.email) as email,
string_agg(CASE WHEN relation.mark IS NULL THEN relation.type::text ELSE CONCAT(relation.type::text, ':', relation.mark::text) END, '/'::text) AS comment,
1 AS valid
FROM hs_integration.contact AS c
JOIN hs_office.relation AS relation ON c.uuid = relation.contactuuid
WHERE (c.defaultprefix != 'hsh' OR (c.partnernumber = 10000 AND c.email = 'hostmaster@hostsharing.net'))
GROUP BY c.uuid;
--//

View File

@ -171,9 +171,3 @@ databaseChangeLog:
file: db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql
- include:
file: db/changelog/9-hs-global/9000-statistics.sql
- include:
file: db/changelog/9-hs-global/9100-hs-integration-schema.sql
- include:
file: db/changelog/9-hs-global/9110-integration-kimai.sql
- include:
file: db/changelog/9-hs-global/9120-integration-znuny.sql

View File

@ -54,7 +54,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
"subscriber:customers-announce"
};
private static final String[] KNOWN_ROLES = ArrayUtils.addAll(
new String[] { "partner", "vip-contact", "ex-partner", "billing", "contractual", "operation", "silent" },
new String[] { "partner", "vip-contact", "ex-partner", "billing", "contractual", "operation" },
SUBSCRIBER_ROLES);
// at least as the number of lines in business_partners.csv from test-data, but less than real data partner count
@ -65,21 +65,18 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
static int relationId = INITIAL_RELATION_ID;
private static final List<Integer> IGNORE_BUSINESS_PARTNERS = Arrays.asList(
512167, // 11139, partner without contractual contact
512170, // 11142, partner without contractual contact
511725, // 10764, partner without contractual contact
// 512171, // 11143, partner without partner contact -- exception
-1
);
private static final List<Integer> IGNORE_CONTACTS = Arrays.asList(
90547, // Kontakt hat keine Rolle
-1
);
private static final Map<Integer, HsOfficePersonType> PERSON_TYPES_BY_CONTACT = Map.of(
90072, HsOfficePersonType.NATURAL_PERSON,
90641, HsOfficePersonType.LEGAL_PERSON,
90368, HsOfficePersonType.LEGAL_PERSON,
90564, HsOfficePersonType.NATURAL_PERSON,
-1, HsOfficePersonType.LEGAL_PERSON
);
static Map<Integer, HsOfficeContactRealEntity> contacts = new WriteOnceMap<>();
static Map<Integer, HsOfficePersonEntity> persons = new WriteOnceMap<>();
static Map<Integer, HsOfficePartnerEntity> partners = new WriteOnceMap<>();
@ -195,56 +192,56 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
assertThat(toJsonFormattedString(partners)).isEqualToIgnoringWhitespace("""
{
100=partner(P-10003: ?? Michael Mellis, Herr Michael Mellis, Michael Mellis),
120=partner(P-10020: LP JM GmbH, Herr Philip Meyer-Contract, JM GmbH),
122=partner(P-11022: ?? Test PS, Petra Schmidt, Test PS),
132=partner(P-10152: ?? Ragnar IT-Beratung, Herr Ragnar Richter, Ragnar IT-Beratung),
190=partner(P-19090: NP Camus, Cecilia, Frau Cecilia Camus),
100=partner(P-10003: ?? Michael Mellis, Herr Michael Mellis , Michael Mellis),
120=partner(P-10020: LP JM GmbH, Herr Philip Meyer-Contract , JM GmbH),
122=partner(P-11022: ?? Test PS, Petra Schmidt , Test PS),
132=partner(P-10152: ?? Ragnar IT-Beratung, Herr Ragnar Richter , Ragnar IT-Beratung),
190=partner(P-19090: NP Camus, Cecilia, Frau Cecilia Camus ),
199=partner(P-19999: null null, null),
213=partner(P-10000: LP Hostsharing e.G., Firma Hostmaster Hostsharing, Hostsharing e.G.),
541=partner(P-11018: ?? Wasserwerk Südholstein, Frau Christiane Milberg, Wasserwerk Südholstein),
542=partner(P-11019: ?? Das Perfekte Haus, Herr Richard Wiese, Das Perfekte Haus)
213=partner(P-10000: LP Hostsharing e.G., Firma Hostmaster Hostsharing , Hostsharing e.G.),
541=partner(P-11018: ?? Wasserwerk Südholstein, Frau Christiane Milberg , Wasserwerk Südholstein),
542=partner(P-11019: ?? Das Perfekte Haus, Herr Richard Wiese , Das Perfekte Haus)
}
""");
assertThat(toJsonFormattedString(contacts)).isEqualToIgnoringWhitespace("""
{
100=contact(caption='Herr Michael Mellis, Michael Mellis', emailAddresses='{ "main": "michael@Mellis.example.org"}'),
100=contact(caption='Herr Michael Mellis , Michael Mellis', emailAddresses='{ "main": "michael@Mellis.example.org"}'),
1200=contact(caption='JM e.K.', emailAddresses='{ "main": "jm-ex-partner@example.org"}'),
1201=contact(caption='Frau Dr. Jenny Meyer-Billing, JM GmbH', emailAddresses='{ "main": "jm-billing@example.org"}'),
1202=contact(caption='Herr Andrew Meyer-Operation, JM GmbH', emailAddresses='{ "main": "am-operation@example.org"}'),
1203=contact(caption='Herr Philip Meyer-Contract, JM GmbH', emailAddresses='{ "main": "pm-partner@example.org"}'),
1204=contact(caption='Frau Tammy Meyer-VIP, JM GmbH', emailAddresses='{ "main": "tm-vip@example.org"}'),
1301=contact(caption='Petra Schmidt, Test PS', emailAddresses='{ "main": "ps@example.com"}'),
132=contact(caption='Herr Ragnar Richter, Ragnar IT-Beratung', emailAddresses='{ "main": "hostsharing@ragnar-richter.de"}'),
1401=contact(caption='Frau Frauke Fanninga', emailAddresses='{ "main": "ff@example.org"}'),
1501=contact(caption='Frau Cecilia Camus', emailAddresses='{ "main": "cc@example.org"}'),
212=contact(caption='Firma Hostmaster Hostsharing, Hostsharing e.G.', emailAddresses='{ "main": "hostmaster@hostsharing.net"}'),
90436=contact(caption='Frau Christiane Milberg, Wasserwerk Südholstein', emailAddresses='{ "main": "rechnung@ww-sholst.example.org"}'),
90437=contact(caption='Herr Richard Wiese, Das Perfekte Haus', emailAddresses='{ "main": "admin@das-perfekte-haus.example.org"}'),
90438=contact(caption='Herr Karim Metzger, Wasswerwerk Südholstein', emailAddresses='{ "main": "karim.metzger@ww-sholst.example.org"}'),
90590=contact(caption='Herr Inhaber R. Wiese, Das Perfekte Haus', emailAddresses='{ "main": "515217@kkemail.example.org"}'),
90629=contact(caption='Ragnar Richter', emailAddresses='{ "main": "mail@ragnar-richter..example.org"}'),
90677=contact(caption='Eike Henning', emailAddresses='{ "main": "hostsharing@eike-henning..example.org"}'),
90698=contact(caption='Jan Henning', emailAddresses='{ "main": "mail@jan-henning.example.org"}')
1201=contact(caption='Frau Dr. Jenny Meyer-Billing , JM GmbH', emailAddresses='{ "main": "jm-billing@example.org"}'),
1202=contact(caption='Herr Andrew Meyer-Operation , JM GmbH', emailAddresses='{ "main": "am-operation@example.org"}'),
1203=contact(caption='Herr Philip Meyer-Contract , JM GmbH', emailAddresses='{ "main": "pm-partner@example.org"}'),
1204=contact(caption='Frau Tammy Meyer-VIP , JM GmbH', emailAddresses='{ "main": "tm-vip@example.org"}'),
1301=contact(caption='Petra Schmidt , Test PS', emailAddresses='{ "main": "ps@example.com"}'),
132=contact(caption='Herr Ragnar Richter , Ragnar IT-Beratung', emailAddresses='{ "main": "hostsharing@ragnar-richter.de"}'),
1401=contact(caption='Frau Frauke Fanninga ', emailAddresses='{ "main": "ff@example.org"}'),
1501=contact(caption='Frau Cecilia Camus ', emailAddresses='{ "main": "cc@example.org"}'),
212=contact(caption='Firma Hostmaster Hostsharing , Hostsharing e.G.', emailAddresses='{ "main": "hostmaster@hostsharing.net"}'),
90436=contact(caption='Frau Christiane Milberg , Wasserwerk Südholstein', emailAddresses='{ "main": "rechnung@ww-sholst.example.org"}'),
90437=contact(caption='Herr Richard Wiese , Das Perfekte Haus', emailAddresses='{ "main": "admin@das-perfekte-haus.example.org"}'),
90438=contact(caption='Herr Karim Metzger , Wasswerwerk Südholstein', emailAddresses='{ "main": "karim.metzger@ww-sholst.example.org"}'),
90590=contact(caption='Herr Inhaber R. Wiese , Das Perfekte Haus', emailAddresses='{ "main": "515217@kkemail.example.org"}'),
90629=contact(caption='Ragnar Richter ', emailAddresses='{ "main": "mail@ragnar-richter..example.org"}'),
90677=contact(caption='Eike Henning ', emailAddresses='{ "main": "hostsharing@eike-henning..example.org"}'),
90698=contact(caption='Jan Henning ', emailAddresses='{ "main": "mail@jan-henning.example.org"}')
}
""");
assertThat(toJsonFormattedString(persons)).isEqualToIgnoringWhitespace("""
{
100=person(personType='??', tradeName='Michael Mellis', salutation='Herr', familyName='Mellis', givenName='Michael'),
100=person(personType='??', tradeName='Michael Mellis', familyName='Mellis', givenName='Michael'),
1200=person(personType='LP', tradeName='JM e.K.'),
1201=person(personType='LP', tradeName='JM GmbH', salutation='Frau', title='Dr.', familyName='Meyer-Billing', givenName='Jenny'),
1202=person(personType='LP', tradeName='JM GmbH', salutation='Herr', familyName='Meyer-Operation', givenName='Andrew'),
1203=person(personType='LP', tradeName='JM GmbH', salutation='Herr', familyName='Meyer-Contract', givenName='Philip'),
1204=person(personType='LP', tradeName='JM GmbH', salutation='Frau', familyName='Meyer-VIP', givenName='Tammy'),
1201=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Billing', givenName='Jenny'),
1202=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Operation', givenName='Andrew'),
1203=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Contract', givenName='Philip'),
1204=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-VIP', givenName='Tammy'),
1301=person(personType='??', tradeName='Test PS', familyName='Schmidt', givenName='Petra'),
132=person(personType='??', tradeName='Ragnar IT-Beratung', salutation='Herr', familyName='Richter', givenName='Ragnar'),
1401=person(personType='NP', salutation='Frau', familyName='Fanninga', givenName='Frauke'),
1501=person(personType='NP', salutation='Frau', familyName='Camus', givenName='Cecilia'),
212=person(personType='LP', tradeName='Hostsharing e.G.', salutation='Firma', familyName='Hostsharing', givenName='Hostmaster'),
90436=person(personType='??', tradeName='Wasserwerk Südholstein', salutation='Frau', familyName='Milberg', givenName='Christiane'),
90437=person(personType='??', tradeName='Das Perfekte Haus', salutation='Herr', familyName='Wiese', givenName='Richard'),
90438=person(personType='??', tradeName='Wasswerwerk Südholstein', salutation='Herr', familyName='Metzger', givenName='Karim'),
90590=person(personType='??', tradeName='Das Perfekte Haus', salutation='Herr', familyName='Wiese', givenName='Inhaber R.'),
132=person(personType='??', tradeName='Ragnar IT-Beratung', familyName='Richter', givenName='Ragnar'),
1401=person(personType='NP', familyName='Fanninga', givenName='Frauke'),
1501=person(personType='NP', familyName='Camus', givenName='Cecilia'),
212=person(personType='LP', tradeName='Hostsharing e.G.', familyName='Hostsharing', givenName='Hostmaster'),
90436=person(personType='??', tradeName='Wasserwerk Südholstein', familyName='Milberg', givenName='Christiane'),
90437=person(personType='??', tradeName='Das Perfekte Haus', familyName='Wiese', givenName='Richard'),
90438=person(personType='??', tradeName='Wasswerwerk Südholstein', familyName='Metzger', givenName='Karim'),
90590=person(personType='??', tradeName='Das Perfekte Haus', familyName='Wiese', givenName='Inhaber R.'),
90629=person(personType='NP', familyName='Richter', givenName='Ragnar'),
90677=person(personType='NP', familyName='Henning', givenName='Eike'),
90698=person(personType='NP', familyName='Henning', givenName='Jan')
@ -275,81 +272,71 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
""");
assertThat(toJsonFormattedString(relations)).isEqualToIgnoringWhitespace("""
{
2000000=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Michael Mellis', contact='Herr Michael Mellis, Michael Mellis'),
2000001=rel(anchor='?? Michael Mellis', type='DEBITOR', holder='?? Michael Mellis', contact='Herr Michael Mellis, Michael Mellis'),
2000002=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter, Ragnar IT-Beratung'),
2000003=rel(anchor='?? Ragnar IT-Beratung', type='DEBITOR', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter, Ragnar IT-Beratung'),
2000004=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing, Hostsharing e.G.'),
2000005=rel(anchor='LP Hostsharing e.G.', type='DEBITOR', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing, Hostsharing e.G.'),
2000006=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg, Wasserwerk Südholstein'),
2000007=rel(anchor='?? Wasserwerk Südholstein', type='DEBITOR', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg, Wasserwerk Südholstein'),
2000008=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese, Das Perfekte Haus'),
2000009=rel(anchor='?? Das Perfekte Haus', type='DEBITOR', holder='?? Das Perfekte Haus', contact='Herr Inhaber R. Wiese, Das Perfekte Haus'),
2000010=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract, JM GmbH'),
2000011=rel(anchor='LP JM GmbH', type='DEBITOR', holder='LP JM GmbH', contact='Frau Dr. Jenny Meyer-Billing, JM GmbH'),
2000012=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Test PS', contact='Petra Schmidt, Test PS'),
2000013=rel(anchor='?? Test PS', type='DEBITOR', holder='?? Test PS', contact='Petra Schmidt, Test PS'),
2000014=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus'),
2000015=rel(anchor='NP Camus, Cecilia', type='DEBITOR', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus'),
2000000=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'),
2000001=rel(anchor='?? Michael Mellis', type='DEBITOR', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'),
2000002=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'),
2000003=rel(anchor='?? Ragnar IT-Beratung', type='DEBITOR', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'),
2000004=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing , Hostsharing e.G.'),
2000005=rel(anchor='LP Hostsharing e.G.', type='DEBITOR', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing , Hostsharing e.G.'),
2000006=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'),
2000007=rel(anchor='?? Wasserwerk Südholstein', type='DEBITOR', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'),
2000008=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'),
2000009=rel(anchor='?? Das Perfekte Haus', type='DEBITOR', holder='?? Das Perfekte Haus', contact='Herr Inhaber R. Wiese , Das Perfekte Haus'),
2000010=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'),
2000011=rel(anchor='LP JM GmbH', type='DEBITOR', holder='LP JM GmbH', contact='Frau Dr. Jenny Meyer-Billing , JM GmbH'),
2000012=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Test PS', contact='Petra Schmidt , Test PS'),
2000013=rel(anchor='?? Test PS', type='DEBITOR', holder='?? Test PS', contact='Petra Schmidt , Test PS'),
2000014=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '),
2000015=rel(anchor='NP Camus, Cecilia', type='DEBITOR', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '),
2000016=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='null null, null'),
2000017=rel(anchor='null null, null', type='DEBITOR'),
2000018=rel(anchor='LP Hostsharing e.G.', type='OPERATIONS_ALERT', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing, Hostsharing e.G.'),
2000019=rel(anchor='LP Hostsharing e.G.', type='OPERATIONS', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing, Hostsharing e.G.'),
2000020=rel(anchor='LP Hostsharing e.G.', type='REPRESENTATIVE', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing, Hostsharing e.G.'),
2000021=rel(anchor='?? Michael Mellis', type='OPERATIONS_ALERT', holder='?? Michael Mellis', contact='Herr Michael Mellis, Michael Mellis'),
2000022=rel(anchor='?? Michael Mellis', type='OPERATIONS', holder='?? Michael Mellis', contact='Herr Michael Mellis, Michael Mellis'),
2000023=rel(anchor='?? Michael Mellis', type='REPRESENTATIVE', holder='?? Michael Mellis', contact='Herr Michael Mellis, Michael Mellis'),
2000024=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='operations-discussion', holder='?? Michael Mellis', contact='Herr Michael Mellis, Michael Mellis'),
2000025=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='operations-announce', holder='?? Michael Mellis', contact='Herr Michael Mellis, Michael Mellis'),
2000026=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='generalversammlung', holder='?? Michael Mellis', contact='Herr Michael Mellis, Michael Mellis'),
2000027=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='members-announce', holder='?? Michael Mellis', contact='Herr Michael Mellis, Michael Mellis'),
2000028=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='members-discussion', holder='?? Michael Mellis', contact='Herr Michael Mellis, Michael Mellis'),
2000029=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS_ALERT', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter, Ragnar IT-Beratung'),
2000030=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter, Ragnar IT-Beratung'),
2000031=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-discussion', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter, Ragnar IT-Beratung'),
2000032=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-announce', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter, Ragnar IT-Beratung'),
2000033=rel(anchor='LP JM GmbH', type='EX_PARTNER', holder='LP JM e.K.', contact='JM e.K.'),
2000034=rel(anchor='LP JM GmbH', type='OPERATIONS_ALERT', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation, JM GmbH'),
2000035=rel(anchor='LP JM GmbH', type='OPERATIONS', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation, JM GmbH'),
2000036=rel(anchor='LP JM GmbH', type='VIP_CONTACT', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation, JM GmbH'),
2000037=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='operations-announce', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation, JM GmbH'),
2000038=rel(anchor='LP JM GmbH', type='REPRESENTATIVE', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract, JM GmbH'),
2000039=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='members-announce', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract, JM GmbH'),
2000040=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='customers-announce', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract, JM GmbH'),
2000041=rel(anchor='LP JM GmbH', type='VIP_CONTACT', holder='LP JM GmbH', contact='Frau Tammy Meyer-VIP, JM GmbH'),
2000042=rel(anchor='?? Test PS', type='OPERATIONS_ALERT', holder='?? Test PS', contact='Petra Schmidt, Test PS'),
2000043=rel(anchor='?? Test PS', type='OPERATIONS', holder='?? Test PS', contact='Petra Schmidt, Test PS'),
2000044=rel(anchor='?? Test PS', type='REPRESENTATIVE', holder='?? Test PS', contact='Petra Schmidt, Test PS'),
2000045=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='operations-announce', holder='NP Fanninga, Frauke', contact='Frau Frauke Fanninga'),
2000046=rel(anchor='NP Camus, Cecilia', type='OPERATIONS_ALERT', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus'),
2000047=rel(anchor='NP Camus, Cecilia', type='OPERATIONS', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus'),
2000048=rel(anchor='NP Camus, Cecilia', type='REPRESENTATIVE', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus'),
2000049=rel(anchor='?? Wasserwerk Südholstein', type='REPRESENTATIVE', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg, Wasserwerk Südholstein'),
2000050=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='generalversammlung', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg, Wasserwerk Südholstein'),
2000051=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='members-announce', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg, Wasserwerk Südholstein'),
2000052=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='members-discussion', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg, Wasserwerk Südholstein'),
2000053=rel(anchor='?? Das Perfekte Haus', type='OPERATIONS_ALERT', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese, Das Perfekte Haus'),
2000054=rel(anchor='?? Das Perfekte Haus', type='OPERATIONS', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese, Das Perfekte Haus'),
2000055=rel(anchor='?? Das Perfekte Haus', type='REPRESENTATIVE', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese, Das Perfekte Haus'),
2000056=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='operations-discussion', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese, Das Perfekte Haus'),
2000057=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='operations-announce', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese, Das Perfekte Haus'),
2000058=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='generalversammlung', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese, Das Perfekte Haus'),
2000059=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='members-announce', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese, Das Perfekte Haus'),
2000060=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='members-discussion', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese, Das Perfekte Haus'),
2000061=rel(anchor='?? Wasserwerk Südholstein', type='OPERATIONS_ALERT', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger, Wasswerwerk Südholstein'),
2000062=rel(anchor='?? Wasserwerk Südholstein', type='OPERATIONS', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger, Wasswerwerk Südholstein'),
2000063=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='operations-discussion', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger, Wasswerwerk Südholstein'),
2000064=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='operations-announce', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger, Wasswerwerk Südholstein'),
2000065=rel(anchor='?? Ragnar IT-Beratung', type='REPRESENTATIVE', holder='NP Richter, Ragnar', contact='Ragnar Richter'),
2000066=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='generalversammlung', holder='NP Richter, Ragnar', contact='Ragnar Richter'),
2000067=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='members-announce', holder='NP Richter, Ragnar', contact='Ragnar Richter'),
2000068=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='members-discussion', holder='NP Richter, Ragnar', contact='Ragnar Richter'),
2000069=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS_ALERT', holder='NP Henning, Eike', contact='Eike Henning'),
2000070=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS', holder='NP Henning, Eike', contact='Eike Henning'),
2000071=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-discussion', holder='NP Henning, Eike', contact='Eike Henning'),
2000072=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-announce', holder='NP Henning, Eike', contact='Eike Henning'),
2000073=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS_ALERT', holder='NP Henning, Jan', contact='Jan Henning'),
2000074=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS', holder='NP Henning, Jan', contact='Jan Henning')
2000018=rel(anchor='LP Hostsharing e.G.', type='OPERATIONS', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing , Hostsharing e.G.'),
2000019=rel(anchor='LP Hostsharing e.G.', type='REPRESENTATIVE', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing , Hostsharing e.G.'),
2000020=rel(anchor='?? Michael Mellis', type='OPERATIONS', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'),
2000021=rel(anchor='?? Michael Mellis', type='REPRESENTATIVE', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'),
2000022=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='operations-discussion', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'),
2000023=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='operations-announce', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'),
2000024=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='generalversammlung', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'),
2000025=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='members-announce', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'),
2000026=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='members-discussion', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'),
2000027=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'),
2000028=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-discussion', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'),
2000029=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-announce', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'),
2000030=rel(anchor='LP JM GmbH', type='EX_PARTNER', holder='LP JM e.K.', contact='JM e.K.'),
2000031=rel(anchor='LP JM GmbH', type='OPERATIONS', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'),
2000032=rel(anchor='LP JM GmbH', type='VIP_CONTACT', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'),
2000033=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='operations-announce', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'),
2000034=rel(anchor='LP JM GmbH', type='REPRESENTATIVE', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'),
2000035=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='members-announce', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'),
2000036=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='customers-announce', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'),
2000037=rel(anchor='LP JM GmbH', type='VIP_CONTACT', holder='LP JM GmbH', contact='Frau Tammy Meyer-VIP , JM GmbH'),
2000038=rel(anchor='?? Test PS', type='OPERATIONS', holder='?? Test PS', contact='Petra Schmidt , Test PS'),
2000039=rel(anchor='?? Test PS', type='REPRESENTATIVE', holder='?? Test PS', contact='Petra Schmidt , Test PS'),
2000040=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='operations-announce', holder='NP Fanninga, Frauke', contact='Frau Frauke Fanninga '),
2000041=rel(anchor='NP Camus, Cecilia', type='OPERATIONS', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '),
2000042=rel(anchor='NP Camus, Cecilia', type='REPRESENTATIVE', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '),
2000043=rel(anchor='?? Wasserwerk Südholstein', type='REPRESENTATIVE', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'),
2000044=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='generalversammlung', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'),
2000045=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='members-announce', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'),
2000046=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='members-discussion', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'),
2000047=rel(anchor='?? Das Perfekte Haus', type='OPERATIONS', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'),
2000048=rel(anchor='?? Das Perfekte Haus', type='REPRESENTATIVE', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'),
2000049=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='operations-discussion', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'),
2000050=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='operations-announce', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'),
2000051=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='generalversammlung', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'),
2000052=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='members-announce', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'),
2000053=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='members-discussion', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'),
2000054=rel(anchor='?? Wasserwerk Südholstein', type='OPERATIONS', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger , Wasswerwerk Südholstein'),
2000055=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='operations-discussion', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger , Wasswerwerk Südholstein'),
2000056=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='operations-announce', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger , Wasswerwerk Südholstein'),
2000057=rel(anchor='?? Ragnar IT-Beratung', type='REPRESENTATIVE', holder='NP Richter, Ragnar', contact='Ragnar Richter '),
2000058=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='generalversammlung', holder='NP Richter, Ragnar', contact='Ragnar Richter '),
2000059=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='members-announce', holder='NP Richter, Ragnar', contact='Ragnar Richter '),
2000060=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='members-discussion', holder='NP Richter, Ragnar', contact='Ragnar Richter '),
2000061=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS', holder='NP Henning, Eike', contact='Eike Henning '),
2000062=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-discussion', holder='NP Henning, Eike', contact='Eike Henning '),
2000063=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-announce', holder='NP Henning, Eike', contact='Eike Henning '),
2000064=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS', holder='NP Henning, Jan', contact='Jan Henning ')
}
""");
}
@ -449,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),
@ -515,7 +502,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
// this happens if a natural person is marked as 'contractual' for itself
final var idsToRemove = new HashSet<Integer>();
relations.forEach((id, r) -> {
if (r.getType() == HsOfficeRelationType.REPRESENTATIVE && r.getHolder() == r.getAnchor()) {
if (r.getHolder() == r.getAnchor()) {
idsToRemove.add(id);
}
});
@ -683,7 +670,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
jpaAttempt.transacted(() -> {
context(rbacSuperuser);
coopShares.forEach(this::persist);
updateLegacyIds(coopShares, "hs_office.coopsharetx_legacy_id", "member_share_id");
updateLegacyIds(coopShares, "hs_office.coopsharestransaction_legacy_id", "member_share_id");
}).assertSuccessful();
@ -877,8 +864,20 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
.comment(rec.getString("comment"))
.reference(member.getMemberNumber().toString())
.build();
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 &&
@ -888,10 +887,21 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
.orElseThrow(() -> new IllegalStateException(
"cannot determine asset reverse entry for reversal " + assetTransaction));
assetTransaction.setRevertedAssetTx(revertedAssetTx);
//revertedAssetTx.setAssetReversalTx(assetTransaction);
}
coopAssets.put(rec.getInteger("member_asset_id"), 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) {
@ -971,9 +981,6 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
HsOfficePersonEntity contactPerson = partnerPerson;
if (!StringUtils.equals(rec.getString("firma"), partnerPerson.getTradeName()) ||
partnerPerson.getPersonType() != determinePersonType(rec) ||
!StringUtils.equals(rec.getString("title"), partnerPerson.getTitle()) ||
!StringUtils.equals(rec.getString("salut"), partnerPerson.getSalutation()) ||
!StringUtils.equals(rec.getString("first_name"), partnerPerson.getGivenName()) ||
!StringUtils.equals(rec.getString("last_name"), partnerPerson.getFamilyName())) {
contactPerson = addPerson(HsOfficePersonEntity.builder().build(), rec);
@ -992,10 +999,6 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
debitor.getDebitorRel().setContact(contact);
}
if (containsRole(rec, "operation")) {
addRelation(HsOfficeRelationType.OPERATIONS_ALERT, partnerPerson, contactPerson, contact);
addRelation(HsOfficeRelationType.OPERATIONS, partnerPerson, contactPerson, contact);
}
if (containsRole(rec, "silent")) {
addRelation(HsOfficeRelationType.OPERATIONS, partnerPerson, contactPerson, contact);
}
if (containsRole(rec, "contractual")) {
@ -1073,60 +1076,34 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
}
private HsOfficePersonEntity addPerson(final HsOfficePersonEntity person, final Record contactRecord) {
person.setSalutation(contactRecord.getString("salut"));
person.setTitle(contactRecord.getString("title"));
// TODO: title+salutation: add to person
person.setGivenName(contactRecord.getString("first_name"));
person.setFamilyName(contactRecord.getString("last_name"));
person.setTradeName(contactRecord.getString("firma"));
person.setPersonType(determinePersonType(contactRecord));
determinePersonType(person, contactRecord.getString("roles"));
persons.put(contactRecord.getInteger("contact_id"), person);
return person;
}
private static HsOfficePersonType determinePersonType(final Record contactRecord) {
String roles = contactRecord.getString("roles");
String country = contactRecord.getString("country");
String familyName = contactRecord.getString("last_name");
String givenName = contactRecord.getString("first_name");
String tradeName = contactRecord.getString("firma");
if (PERSON_TYPES_BY_CONTACT.containsKey(contactRecord.getInteger("contact_id"))) {
return PERSON_TYPES_BY_CONTACT.get(contactRecord.getInteger("contact_id"));
}
if (tradeName.isBlank() || tradeName.startsWith("verstorben")) {
return HsOfficePersonType.NATURAL_PERSON;
private static void determinePersonType(final HsOfficePersonEntity person, final String roles) {
if (person.getTradeName().isBlank()) {
person.setPersonType(HsOfficePersonType.NATURAL_PERSON);
} else
// contractual && !partner with a firm and a natural person name
// should actually be split up into two persons
// but the legacy database consists such records
if (endsWithWord(tradeName, "OHG", "GbR", "KG", "UG", "PartGmbB", "mbB")) {
return HsOfficePersonType.INCORPORATED_FIRM; // Personengesellschaft. Gesellschafter haften persönlich.
} else if (containsWord(tradeName, "e.K.", "e.G.", "eG", "gGmbH", "GmbH", "mbH", "AG", "e.V.", "eV", "e.V")
|| tradeName.toLowerCase().contains("haftungsbeschränkt")
|| tradeName.toLowerCase().contains("stiftung")
|| tradeName.toLowerCase().contains("stichting")
|| tradeName.toLowerCase().contains("foundation")
|| tradeName.toLowerCase().contains("schule")
|| tradeName.toLowerCase().contains("verein")
|| tradeName.toLowerCase().contains("gewerkschaft")
|| tradeName.toLowerCase().contains("gesellschaft")
|| tradeName.toLowerCase().contains("kirche")
|| tradeName.toLowerCase().contains("fraktion")
|| tradeName.toLowerCase().contains("landkreis")
|| tradeName.toLowerCase().contains("behörde")
|| tradeName.toLowerCase().contains("bundesamt")
|| tradeName.toLowerCase().contains("bezirksamt")
) {
return HsOfficePersonType.LEGAL_PERSON; // Haftungsbeschränkt
} else if (roles.contains("contractual") && !roles.contains("partner") &&
!familyName.isBlank() && !givenName.isBlank()) {
// REPRESENTATIVES are always natural persons
return HsOfficePersonType.NATURAL_PERSON;
if (roles.contains("contractual") && !roles.contains("partner") &&
!person.getFamilyName().isBlank() && !person.getGivenName().isBlank()) {
person.setPersonType(HsOfficePersonType.NATURAL_PERSON);
} else if (endsWithWord(person.getTradeName(), "e.K.", "e.G.", "eG", "GmbH", "AG", "KG")) {
person.setPersonType(HsOfficePersonType.LEGAL_PERSON);
} else if (endsWithWord(person.getTradeName(), "OHG")) {
person.setPersonType(HsOfficePersonType.INCORPORATED_FIRM);
} else if (endsWithWord(person.getTradeName(), "GbR")) {
person.setPersonType(HsOfficePersonType.INCORPORATED_FIRM);
} else {
return HsOfficePersonType.UNKNOWN_PERSON_TYPE;
person.setPersonType(HsOfficePersonType.UNKNOWN_PERSON_TYPE);
}
}
@ -1140,19 +1117,6 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
return false;
}
private static boolean containsWord(final String value, final String... endings) {
final var lowerCaseValue = value.toLowerCase();
for (String ending : endings) {
if (lowerCaseValue.equals(ending.toLowerCase()) ||
lowerCaseValue.startsWith(ending.toLowerCase() + " ") ||
lowerCaseValue.contains(" " + ending.toLowerCase() + " ") ||
lowerCaseValue.endsWith(" " + ending.toLowerCase())) {
return true;
}
}
return false;
}
private void verifyContainsOnlyKnownRoles(final String roles) {
final var allowedRolesSet = stream(KNOWN_ROLES).collect(Collectors.toSet());
final var givenRolesSet = stream(roles.replace(" ", "").split(",")).collect(Collectors.toSet());
@ -1217,13 +1181,13 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
final String firm) {
final var result = new StringBuilder();
if (isNotBlank(salut))
result.append((isBlank(result) ? "" : " ") + salut);
result.append(salut + " ");
if (isNotBlank(title))
result.append((isBlank(result) ? "" : " ") + title);
result.append(title + " ");
if (isNotBlank(firstname))
result.append((isBlank(result) ? "" : " ") + firstname);
result.append(firstname + " ");
if (isNotBlank(lastname))
result.append((isBlank(result) ? "" : " ") + lastname);
result.append(lastname + " ");
if (isNotBlank(firm)) {
result.append((isBlank(result) ? "" : ", ") + firm);
}

View File

@ -173,8 +173,13 @@ public class CsvDataImport extends ContextBasedTest {
//System.out.println("persisting #" + entity.hashCode() + ": " + entity);
em.persist(entity);
// uncomment for debugging purposes
// 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
// }
return entity;
}
@ -255,7 +260,7 @@ public class CsvDataImport extends ContextBasedTest {
em.createNativeQuery("delete from hs_office.coopassettx where true").executeUpdate();
em.createNativeQuery("delete from hs_office.coopassettx_legacy_id where true").executeUpdate();
em.createNativeQuery("delete from hs_office.coopsharetx where true").executeUpdate();
em.createNativeQuery("delete from hs_office.coopsharetx_legacy_id where true").executeUpdate();
em.createNativeQuery("delete from hs_office.coopsharestransaction_legacy_id where true").executeUpdate();
em.createNativeQuery("delete from hs_office.membership where true").executeUpdate();
em.createNativeQuery("delete from hs_office.sepamandate where true").executeUpdate();
em.createNativeQuery("delete from hs_office.sepamandate_legacy_id where true").executeUpdate();
@ -275,7 +280,7 @@ public class CsvDataImport extends ContextBasedTest {
em.createNativeQuery("alter sequence hs_office.contact_legacy_id_seq restart with 1000000000;").executeUpdate();
em.createNativeQuery("alter sequence hs_office.coopassettx_legacy_id_seq restart with 1000000000;")
.executeUpdate();
em.createNativeQuery("alter sequence public.hs_office.coopsharetx_legacy_id_seq restart with 1000000000;")
em.createNativeQuery("alter sequence public.hs_office.coopsharestransaction_legacy_id_seq restart with 1000000000;")
.executeUpdate();
em.createNativeQuery("alter sequence public.hs_office.partner_legacy_id_seq restart with 1000000000;")
.executeUpdate();

View File

@ -8,6 +8,8 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/*
* This 'test' includes the complete legacy 'office' data import.
*
@ -56,4 +58,9 @@ import org.springframework.test.annotation.DirtiesContext;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@ExtendWith(OrderedDependedTestsExtension.class)
public class ImportOfficeData extends BaseOfficeDataImport {
@BeforeEach
void check() {
assertThat(jdbcUrl).isEqualTo("jdbc:tc:postgresql:15.5-bookworm:///importOfficeDataTC");
}
}

View File

@ -25,6 +25,7 @@ class HsOfficeBankAccountControllerRestTest {
Context contextMock;
@MockBean
@SuppressWarnings("unused") // not used in test, but in controller class
StandardMapper mapper;
@MockBean

View File

@ -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

View File

@ -1,50 +1,116 @@
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 net.hostsharing.hsadminng.test.TestUuidGenerator;
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.math.BigDecimal;
import java.time.LocalDate;
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.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.status;
@WebMvcTest(HsOfficeCoopAssetsTransactionController.class)
@Import({ StrictMapper.class, JsonObjectMapperConfiguration.class })
@RunWith(SpringRunner.class)
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
MockMvc mockMvc;
@MockBean
Context contextMock;
@Autowired
@SuppressWarnings("unused") // not used in test, but in controller class
StrictMapper mapper;
@MockBean
StandardMapper mapper;
EntityManagerWrapper emw; // even if not used in test anymore, it's needed by base-class of StrictMapper
@MockBean
HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo;
static final String VALID_INSERT_REQUEST_BODY = """
@MockBean
HsOfficeMembershipRepository membershipRepo;
static final String INSERT_REQUEST_BODY_TEMPLATE = """
{
"membership.uuid": "%s",
"transactionType": "DEPOSIT",
"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());
""".formatted(ORIGIN_MEMBERSHIP_UUID);
enum BadRequestTestCases {
MEMBERSHIP_UUID_MISSING(
@ -65,8 +131,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 +139,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")
@ -104,13 +182,16 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
}
String givenRequestBody() {
return givenBodyTransformation.apply(jsonObject(VALID_INSERT_REQUEST_BODY)).toString();
return givenBodyTransformation.apply(jsonObject(INSERT_REQUEST_BODY_TEMPLATE)).toString();
}
}
@ParameterizedTest
@EnumSource(BadRequestTestCases.class)
void respondWithBadRequest(final BadRequestTestCases testCase) throws Exception {
// HOWTO: run just a single test-case in a data-driven test-method
// org.assertj.core.api.Assumptions.assumeThat(
// testCase == ADOPTING_MEMBERSHIP_NUMBER_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE).isTrue();
// when
mockMvc.perform(MockMvcRequestBuilders
@ -127,4 +208,160 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
.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);
}
}

View File

@ -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();

View File

@ -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)");
}
}

View File

@ -30,6 +30,7 @@ class HsOfficeCoopSharesTransactionControllerRestTest {
Context contextMock;
@MockBean
@SuppressWarnings("unused") // not used in test, but in controller class
StandardMapper mapper;
@MockBean

View File

@ -40,7 +40,7 @@ class HsOfficeCoopSharesTransactionEntityUnitTest {
}
@Test
void toStringWithRevertedAssetTxContainsRevertedAssetTx() {
void toStringWithRelatedAssetTxContainsRelatedAssetTx() {
givenCoopSharesTransaction.setRevertedShareTx(givenCoopShareReversalTransaction);
final var result = givenCoopSharesTransaction.toString();

View File

@ -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;
@ -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.UnsubscribeFromMailinglist;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.IgnoreOnFailure;
import net.hostsharing.hsadminng.test.IgnoreOnFailureExtension;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
@ -51,6 +55,7 @@ import org.springframework.test.annotation.DirtiesContext;
)
@DirtiesContext
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@ExtendWith(IgnoreOnFailureExtension.class)
class HsOfficeScenarioTests extends ScenarioTest {
@Test
@ -77,8 +82,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 +341,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 +365,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 +380,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 +393,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")
@ -398,9 +403,9 @@ class HsOfficeScenarioTests extends ScenarioTest {
}
@Test
@Order(4302)
@Requires("Coop-Assets DEPOSIT Transaction")
@Produces("Coop-Assets DISBURSAL Transaction")
@Order(4303)
@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 +416,33 @@ class HsOfficeScenarioTests extends ScenarioTest {
.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
@Order(4900)
@Requires("Membership: M-3101000 - Test AG")

View File

@ -36,6 +36,7 @@ import java.util.function.Supplier;
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.KEEP_COMMENTS;
import static net.hostsharing.hsadminng.test.DebuggerDetection.isDebuggerAttached;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.platform.commons.util.StringUtils.isBlank;
@ -151,7 +152,7 @@ public abstract class UseCase<T extends UseCase<?>> {
.GET()
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("current-subject", ScenarioTest.RUN_AS_USER)
.timeout(Duration.ofSeconds(10))
.timeout(seconds(10))
.build();
final var response = client.send(request, BodyHandlers.ofString());
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))
.header("Content-Type", "application/json")
.header("current-subject", ScenarioTest.RUN_AS_USER)
.timeout(Duration.ofSeconds(10))
.timeout(seconds(10))
.build();
final var response = client.send(request, BodyHandlers.ofString());
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))
.header("Content-Type", "application/json")
.header("current-subject", ScenarioTest.RUN_AS_USER)
.timeout(Duration.ofSeconds(10))
.timeout(seconds(10))
.build();
final var response = client.send(request, BodyHandlers.ofString());
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))
.header("Content-Type", "application/json")
.header("current-subject", ScenarioTest.RUN_AS_USER)
.timeout(Duration.ofSeconds(10))
.timeout(seconds(10))
.build();
final var response = client.send(request, BodyHandlers.ofString());
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 {
@Getter

View File

@ -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();
}

View File

@ -32,7 +32,8 @@ public abstract class CreateCoopAssetsTransaction extends UseCase<CreateCoopAsse
"assetValue": ${assetValue},
"comment": ${comment},
"valueDate": ${transactionDate},
"revertedAssetTx.uuid": ${revertedAssetTx???}
"revertedAssetTx.uuid": ${revertedAssetTx???},
"adoptingMembership.memberNumber": ${adoptingMemberNumber???}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)

View File

@ -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);
}
}

View File

@ -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"));
}
}

View File

@ -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 "";
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -36,11 +36,13 @@ dump "select sepa_mandat_id, bp_id, bank_customer, bank_name, bank_iban, bank_bi
dump "select member_asset_id, bp_id, date, action, amount, comment
from member_asset
WHERE bp_id NOT IN (511912)
order by member_asset_id" \
"office/asset_transactions.csv"
dump "select member_share_id, bp_id, date, action, quantity, comment
from member_share
WHERE bp_id NOT IN (511912)
order by member_share_id" \
"office/share_transactions.csv"
@ -83,7 +85,7 @@ dump "select domain_id, domain_name, domain_since, domain_dns_master, domain_own
dump "select emailaddr_id, domain_id, localpart, subdomain, target
from emailaddr
order by emailaddr_id" \
"hosting/emailaddr.csv"
"emailaddr.csv"
dump "select emailalias_id, pac_id, name, target
from emailalias