Compare commits

..

1 Commits

Author SHA1 Message Date
d7caf3b0f8 TP-20240927-importfixes (#115)
Co-authored-by: Timotheus Pokorra <timotheus.pokorra@solidcharity.com>
Co-authored-by: Dev und Test fuer hsadminng <hsh03-hsngdev@h50.hostsharing.net>
Reviewed-on: #115
Reviewed-by: Michael Hoennig <michael.hoennig@hostsharing.net>
Co-authored-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
Co-committed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
2024-11-21 10:27:34 +01:00
40 changed files with 454 additions and 1094 deletions

View File

@ -8,20 +8,12 @@ gradleWrapper () {
return 1 return 1
fi 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) TEMPFILE=$(mktemp /tmp/gw.XXXXXX)
unbuffer ./gradlew "$@" | tee $TEMPFILE unbuffer ./gradlew "$@" | tee $TEMPFILE
echo echo
grep --color=never "Report:" $TEMPFILE grep --color=never "Report:" $TEMPFILE
rm $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 () { 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, The basic idea is always to always have a fixed set of ordered role-types which apply for all DB-tables under RBAC,
e.g. OWNER>ADMIN>AGENT\[>PROXY?\]>TENENT>REFERRER. e.g. OWNER>ADMIN>AGENT\[>PROXY?\]>TENENT>REFERRER.
Grants between these for the same DB-row would be implicit by order comparison. Grants between these for the same DB-row would be implicit by order comparision.
This way we would get rid of all explicit grants within the same DB-row This way we would get rid of all explicit grants within the same DB-row
and would not need the `rbac.role` table anymore. and would not need the `rbac.role` table anymore.
We would also reduce the depth of the expensive recursive CTE-query. We would also reduce the depth of the expensive recursive CTE-query.
@ -591,20 +591,8 @@ E.g. the uuid of the target main object is often taken from an uuid of a sub-sub
(For now, use `StrictMapper` to avoid this, for the case it happens.) (For now, use `StrictMapper` to avoid this, for the case it happens.)
### Too Many Business-Rules Implemented in Controllers
Some REST-Controllers implement too much code for business-roles.
This should be extracted to services.
## How To ... ## How To ...
Besides the following *How Tos* you can also find several *How Tos* in the source code:
```sh
grep -r HOWTO src
```
### How to Configure .pgpass for the Default PostgreSQL Database? ### How to Configure .pgpass for the Default PostgreSQL Database?
To access the default database schema as used during development, add this line to your `.pgpass` file in your users home directory: To access the default database schema as used during development, add this line to your `.pgpass` file in your users home directory:

View File

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

View File

@ -79,12 +79,26 @@ der Person des VIP-Contact (_Holder_) zur repräsentierten Person (_Anchor_) dar
### Operations-Contact ### Operations-Contact
Ein _Operations-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner technischer Ansprechpartner ist 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`.
Implementiert ist der _Operations-Contact_ als eine besondere Form der [Relation](#Relation) Implementiert ist der _Operations-Contact_ als eine besondere Form der [Relation](#Relation)
der Person des _Operations-Contact_ (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt. 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 ### Subscriber-Contact
Ein _Subscriber-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner eine bestimmte Mailingliste abonniert. Ein _Subscriber-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner eine bestimmte Mailingliste abonniert.

View File

@ -1,15 +1,10 @@
package net.hostsharing.hsadminng.hs.office.coopassets; package net.hostsharing.hsadminng.hs.office.coopassets;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi; 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.*;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionResource; import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource; 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.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.annotation.DateTimeFormat.ISO;
@ -19,19 +14,13 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.persistence.EntityNotFoundException; import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.ValidationException;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.*;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.CLEARING;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.DEPOSIT;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.DISBURSAL;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.LOSS;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.TRANSFER;
@RestController @RestController
public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi { public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi {
@ -40,17 +29,11 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
private Context context; private Context context;
@Autowired @Autowired
private StrictMapper mapper; private StandardMapper mapper;
@Autowired
private EntityManagerWrapper emw;
@Autowired @Autowired
private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo; private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo;
@Autowired
private HsOfficeMembershipRepository membershipRepo;
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> getListOfCoopAssets( public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> getListOfCoopAssets(
@ -66,7 +49,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
fromValueDate, fromValueDate,
toValueDate); toValueDate);
final var resources = mapper.mapList(entities, HsOfficeCoopAssetsTransactionResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); final var resources = mapper.mapList(entities, HsOfficeCoopAssetsTransactionResource.class);
return ResponseEntity.ok(resources); return ResponseEntity.ok(resources);
} }
@ -80,10 +63,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
context.define(currentSubject, assumedRoles); context.define(currentSubject, assumedRoles);
validate(requestBody); validate(requestBody);
final var entityToSave = mapper.map( final var entityToSave = mapper.map(requestBody, HsOfficeCoopAssetsTransactionEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
requestBody,
HsOfficeCoopAssetsTransactionEntity.class,
RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = coopAssetsTransactionRepo.save(entityToSave); final var saved = coopAssetsTransactionRepo.save(entityToSave);
final var uri = final var uri =
@ -91,7 +71,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
.path("/api/hs/office/coopassetstransactions/{id}") .path("/api/hs/office/coopassetstransactions/{id}")
.buildAndExpand(saved.getUuid()) .buildAndExpand(saved.getUuid())
.toUri(); .toUri();
final var mapped = mapper.map(saved, HsOfficeCoopAssetsTransactionResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); final var mapped = mapper.map(saved, HsOfficeCoopAssetsTransactionResource.class);
return ResponseEntity.created(uri).body(mapped); return ResponseEntity.created(uri).body(mapped);
} }
@ -121,7 +101,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
private static void validateDebitTransaction( private static void validateDebitTransaction(
final HsOfficeCoopAssetsTransactionInsertResource requestBody, final HsOfficeCoopAssetsTransactionInsertResource requestBody,
final ArrayList<String> violations) { final ArrayList<String> violations) {
if (List.of(DEPOSIT, HsOfficeCoopAssetsTransactionTypeResource.ADOPTION).contains(requestBody.getTransactionType()) if (List.of(DEPOSIT, ADOPTION).contains(requestBody.getTransactionType())
&& requestBody.getAssetValue().signum() < 0) { && requestBody.getAssetValue().signum() < 0) {
violations.add("for %s, assetValue must be positive but is \"%.2f\"".formatted( violations.add("for %s, assetValue must be positive but is \"%.2f\"".formatted(
requestBody.getTransactionType(), requestBody.getAssetValue())); requestBody.getTransactionType(), requestBody.getAssetValue()));
@ -147,111 +127,10 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
} }
} }
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) -> { final BiConsumer<HsOfficeCoopAssetsTransactionInsertResource, HsOfficeCoopAssetsTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
if ( resource.getRevertedAssetTxUuid() != null ) {
if (resource.getMembershipUuid() != null) { entity.setRevertedAssetTx(coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid())
final HsOfficeMembershipEntity membership = ofNullable(emw.find(HsOfficeMembershipEntity.class, resource.getMembershipUuid())) .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] reverseEntityUuid %s not found".formatted(resource.getRevertedAssetTxUuid()))));
.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,10 +50,8 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
.withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue) .withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue)
.withProp(HsOfficeCoopAssetsTransactionEntity::getReference) .withProp(HsOfficeCoopAssetsTransactionEntity::getReference)
.withProp(HsOfficeCoopAssetsTransactionEntity::getComment) .withProp(HsOfficeCoopAssetsTransactionEntity::getComment)
.withProp(HsOfficeCoopAssetsTransactionEntity::getRevertedAssetTx) .withProp(at -> ofNullable(at.getRevertedAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null))
.withProp(HsOfficeCoopAssetsTransactionEntity::getReversalAssetTx) .withProp(at -> ofNullable(at.getReversalAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null))
.withProp(HsOfficeCoopAssetsTransactionEntity::getAdoptionAssetTx)
.withProp(HsOfficeCoopAssetsTransactionEntity::getTransferAssetTx)
.quotedValues(false); .quotedValues(false);
@Id @Id
@ -97,24 +95,16 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
@Column(name = "comment") @Column(name = "comment")
private String comment; private String comment;
// Optionally, the UUID of the corresponding transaction for a reversal transaction. /**
@OneToOne(cascade = CascadeType.PERSIST) // TODO.impl: can probably be removed after office data migration * Optionally, the UUID of the corresponding transaction for an reversal transaction.
*/
@OneToOne
@JoinColumn(name = "revertedassettxuuid") @JoinColumn(name = "revertedassettxuuid")
private HsOfficeCoopAssetsTransactionEntity revertedAssetTx; private HsOfficeCoopAssetsTransactionEntity revertedAssetTx;
// and the other way around
@OneToOne(mappedBy = "revertedAssetTx") @OneToOne(mappedBy = "revertedAssetTx")
private HsOfficeCoopAssetsTransactionEntity reversalAssetTx; private HsOfficeCoopAssetsTransactionEntity reversalAssetTx;
// Optionally, the UUID of the corresponding transaction for a transfer transaction.
@OneToOne(cascade = CascadeType.PERSIST) // TODO.impl: can probably be removed after office data migration
@JoinColumn(name = "assetadoptiontxuuid")
private HsOfficeCoopAssetsTransactionEntity adoptionAssetTx;
// and the other way around
@OneToOne(mappedBy = "adoptionAssetTx")
private HsOfficeCoopAssetsTransactionEntity transferAssetTx;
@Override @Override
public HsOfficeCoopAssetsTransactionEntity load() { public HsOfficeCoopAssetsTransactionEntity load() {
BaseEntity.super.load(); BaseEntity.super.load();

View File

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

View File

@ -80,6 +80,12 @@ public final class Stringify<B> {
.map(prop -> PropertyValue.of(prop, prop.getter.apply(object))) .map(prop -> PropertyValue.of(prop, prop.getter.apply(object)))
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(PropertyValue::nonEmpty) .filter(PropertyValue::nonEmpty)
.map(propVal -> {
if (propVal.rawValue instanceof Stringifyable stringifyable) {
return new PropertyValue<>(propVal.prop, propVal.rawValue, stringifyable.toShortString());
}
return propVal;
})
.map(propVal -> propName(propVal, "=") + optionallyQuoted(propVal)) .map(propVal -> propName(propVal, "=") + optionallyQuoted(propVal))
.collect(Collectors.joining(separator)); .collect(Collectors.joining(separator));
return idProp != null return idProp != null
@ -125,11 +131,7 @@ public final class Stringify<B> {
private record PropertyValue<B>(Property<B> prop, Object rawValue, String value) { private record PropertyValue<B>(Property<B> prop, Object rawValue, String value) {
static <B> PropertyValue<B> of(Property<B> prop, Object rawValue) { static <B> PropertyValue<B> of(Property<B> prop, Object rawValue) {
return rawValue != null ? new PropertyValue<>(prop, rawValue, toStringOrShortString(rawValue)) : null; return rawValue != null ? new PropertyValue<>(prop, rawValue, rawValue.toString()) : null;
}
private static String toStringOrShortString(final Object rawValue) {
return rawValue instanceof Stringifyable stringifyable ? stringifyable.toShortString() : rawValue.toString();
} }
boolean nonEmpty() { boolean nonEmpty() {

View File

@ -20,15 +20,6 @@ components:
uuid: uuid:
type: string type: string
format: uuid format: uuid
membership.uuid:
type: string
format: uuid
nullable: false
membership.memberNumber:
type: string
minLength: 9
maxLength: 9
pattern: 'M-[0-9]{7}'
transactionType: transactionType:
$ref: '#/components/schemas/HsOfficeCoopAssetsTransactionType' $ref: '#/components/schemas/HsOfficeCoopAssetsTransactionType'
assetValue: assetValue:
@ -41,36 +32,20 @@ components:
type: string type: string
comment: comment:
type: string type: string
adoptionAssetTx:
# a TRANSFER tx must refer to the related ADOPTION tx
$ref: '#/components/schemas/HsOfficeRelatedCoopAssetsTransaction'
transferAssetTx:
# an ADOPTION tx must refer to the related TRANSFER tx
$ref: '#/components/schemas/HsOfficeRelatedCoopAssetsTransaction'
revertedAssetTx: revertedAssetTx:
# a REVERSAL tx must refer to the related tx, which can be of any type but REVERSAL $ref: '#/components/schemas/HsOfficeReferencedCoopAssetsTransaction'
$ref: '#/components/schemas/HsOfficeRelatedCoopAssetsTransaction'
reversalAssetTx: reversalAssetTx:
# a reverted tx, which can be any but REVERSAL, must refer to the related REVERSAL tx $ref: '#/components/schemas/HsOfficeReferencedCoopAssetsTransaction'
$ref: '#/components/schemas/HsOfficeRelatedCoopAssetsTransaction'
HsOfficeRelatedCoopAssetsTransaction: HsOfficeReferencedCoopAssetsTransaction:
description: description:
Similar to `HsOfficeCoopAssetsTransaction` but just the UUID of the related property, to avoid recursive JSON. Similar to `HsOfficeCoopAssetsTransaction` but without the self-referencing properties
(`revertedAssetTx` and `reversalAssetTx`), to avoid recursive JSON.
type: object type: object
properties: properties:
uuid: uuid:
type: string type: string
format: uuid format: uuid
membership.uuid:
type: string
format: uuid
nullable: false
membership.memberNumber:
type: string
minLength: 9
maxLength: 9
pattern: 'M-[0-9]{7}'
transactionType: transactionType:
$ref: '#/components/schemas/HsOfficeCoopAssetsTransactionType' $ref: '#/components/schemas/HsOfficeCoopAssetsTransactionType'
assetValue: assetValue:
@ -83,22 +58,6 @@ components:
type: string type: string
comment: comment:
type: string type: string
adoptionAssetTx.uuid:
description: a TRANSFER tx must refer to the related ADOPTION tx
type: string
format: uuid
transferAssetTx.uuid:
description: an ADOPTION tx must refer to the related TRANSFER tx
type: string
format: uuid
revertedAssetTx.uuid:
description: a REVERSAL tx must refer to the related tx, which can be of any type but REVERSAL
type: string
format: uuid
reversalAssetTx.uuid:
description: a reverted tx, which can be any but REVERSAL, must refer to the related REVERSAL tx
type: string
format: uuid
HsOfficeCoopAssetsTransactionInsert: HsOfficeCoopAssetsTransactionInsert:
type: object type: object
@ -124,14 +83,6 @@ components:
revertedAssetTx.uuid: revertedAssetTx.uuid:
type: string type: string
format: uuid format: uuid
adoptingMembership.uuid:
type: string
format: uuid
adoptingMembership.memberNumber:
type: string
minLength: 9
maxLength: 9
pattern: 'M-[0-9]{7}'
required: required:
- membership.uuid - membership.uuid
- transactionType - transactionType

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ CREATE TYPE hs_office.RelationType AS ENUM (
'DEBITOR', 'DEBITOR',
'VIP_CONTACT', 'VIP_CONTACT',
'OPERATIONS', 'OPERATIONS',
'OPERATIONS_ALERT',
'SUBSCRIBER'); 'SUBSCRIBER');
CREATE CAST (character varying as hs_office.RelationType) WITH INOUT AS IMPLICIT; 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:--// --changeset michael.hoennig:hs-office-coopshares-MIGRATION-mapping endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
CREATE TABLE hs_office.coopsharestransaction_legacy_id CREATE TABLE hs_office.coopsharetx_legacy_id
( (
uuid uuid NOT NULL REFERENCES hs_office.coopsharetx(uuid), uuid uuid NOT NULL REFERENCES hs_office.coopsharetx(uuid),
member_share_id integer NOT NULL member_share_id integer NOT NULL
@ -19,10 +19,10 @@ CREATE TABLE hs_office.coopsharestransaction_legacy_id
--changeset michael.hoennig:hs-office-coopshares-MIGRATION-sequence endDelimiter:--// --changeset michael.hoennig:hs-office-coopshares-MIGRATION-sequence endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS hs_office.coopsharestransaction_legacy_id_seq CREATE SEQUENCE IF NOT EXISTS hs_office.coopsharetx_legacy_id_seq
AS integer AS integer
START 1000000000 START 1000000000
OWNED BY hs_office.coopsharestransaction_legacy_id.member_share_id; OWNED BY hs_office.coopsharetx_legacy_id.member_share_id;
--// --//
@ -30,9 +30,9 @@ CREATE SEQUENCE IF NOT EXISTS hs_office.coopsharestransaction_legacy_id_seq
--changeset michael.hoennig:hs-office-coopshares-MIGRATION-default endDelimiter:--// --changeset michael.hoennig:hs-office-coopshares-MIGRATION-default endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
ALTER TABLE hs_office.coopsharestransaction_legacy_id ALTER TABLE hs_office.coopsharetx_legacy_id
ALTER COLUMN member_share_id ALTER COLUMN member_share_id
SET DEFAULT nextVal('hs_office.coopsharestransaction_legacy_id_seq'); SET DEFAULT nextVal('hs_office.coopsharetx_legacy_id_seq');
--/ --/
@ -41,8 +41,8 @@ ALTER TABLE hs_office.coopsharestransaction_legacy_id
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
CALL base.defineContext('schema-migration'); CALL base.defineContext('schema-migration');
INSERT INTO hs_office.coopsharestransaction_legacy_id(uuid, member_share_id) INSERT INTO hs_office.coopsharetx_legacy_id(uuid, member_share_id)
SELECT uuid, nextVal('hs_office.coopsharestransaction_legacy_id_seq') FROM hs_office.coopsharetx; SELECT uuid, nextVal('hs_office.coopsharetx_legacy_id_seq') FROM hs_office.coopsharetx;
--/ --/
@ -58,8 +58,8 @@ begin
raise exception 'invalid usage of trigger'; raise exception 'invalid usage of trigger';
end if; end if;
INSERT INTO hs_office.coopsharestransaction_legacy_id VALUES INSERT INTO hs_office.coopsharetx_legacy_id VALUES
(NEW.uuid, nextVal('hs_office.coopsharestransaction_legacy_id_seq')); (NEW.uuid, nextVal('hs_office.coopsharetx_legacy_id_seq'));
return NEW; return NEW;
end; $$; end; $$;
@ -83,7 +83,7 @@ begin
raise exception 'invalid usage of trigger'; raise exception 'invalid usage of trigger';
end if; end if;
DELETE FROM hs_office.coopsharestransaction_legacy_id DELETE FROM hs_office.coopsharetx_legacy_id
WHERE uuid = OLD.uuid; WHERE uuid = OLD.uuid;
return OLD; return OLD;

View File

@ -24,8 +24,7 @@ create table if not exists hs_office.coopassettx
valueDate date not null, valueDate date not null,
assetValue numeric(12,2) not null, -- https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_money assetValue numeric(12,2) not null, -- https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_money
reference varchar(48) not null, reference varchar(48) not null,
revertedAssetTxuUid uuid unique REFERENCES hs_office.coopassettx(uuid) DEFERRABLE INITIALLY DEFERRED, revertedAssetTxUuid uuid unique REFERENCES hs_office.coopassettx(uuid) DEFERRABLE INITIALLY DEFERRED,
assetAdoptionTxUuid uuid unique REFERENCES hs_office.coopassettx(uuid) DEFERRABLE INITIALLY DEFERRED,
comment varchar(512) comment varchar(512)
); );
--// --//
@ -36,20 +35,9 @@ create table if not exists hs_office.coopassettx
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
alter table hs_office.coopassettx alter table hs_office.coopassettx
add constraint reversal_asset_tx_must_have_reverted_asset_tx add constraint reverse_entry_missing
check (transactionType <> 'REVERSAL' or revertedAssetTxuUid is not null); check ( transactionType = 'REVERSAL' and revertedAssetTxUuid is not null
or transactionType <> 'REVERSAL' and revertedAssetTxUuid is null);
alter table hs_office.coopassettx
add constraint non_reversal_asset_tx_must_not_have_reverted_asset_tx
check (transactionType = 'REVERSAL' or revertedAssetTxuUid is null or transactionType = 'REVERSAL');
alter table hs_office.coopassettx
add constraint transfer_asset_tx_must_have_adopted_asset_tx
check (transactionType <> 'TRANSFER' or assetAdoptionTxUuid is not null);
alter table hs_office.coopassettx
add constraint non_transfer_asset_tx_must_not_have_adopted_asset_tx
check (transactionType = 'TRANSFER' or assetAdoptionTxUuid is null);
--// --//
-- ============================================================================ -- ============================================================================

View File

@ -15,9 +15,7 @@ create or replace procedure hs_office.coopassettx_create_test_data(
language plpgsql as $$ language plpgsql as $$
declare declare
membership hs_office.membership; membership hs_office.membership;
invalidLossTx uuid; lossEntryUuid uuid;
transferTx uuid;
adoptionTx uuid;
begin begin
select m.uuid select m.uuid
from hs_office.membership m from hs_office.membership m
@ -27,18 +25,14 @@ begin
into membership; into membership;
raise notice 'creating test coopAssetsTransaction: %', givenPartnerNumber || givenMemberNumberSuffix; raise notice 'creating test coopAssetsTransaction: %', givenPartnerNumber || givenMemberNumberSuffix;
invalidLossTx := uuid_generate_v4(); lossEntryUuid := uuid_generate_v4();
transferTx := uuid_generate_v4();
adoptionTx := uuid_generate_v4();
insert insert
into hs_office.coopassettx(uuid, membershipuuid, transactiontype, valuedate, assetvalue, reference, comment, revertedAssetTxuUid, assetAdoptionTxUuid) into hs_office.coopassettx(uuid, membershipuuid, transactiontype, valuedate, assetvalue, reference, comment, revertedAssetTxUuid)
values values
(uuid_generate_v4(), membership.uuid, 'DEPOSIT', '2010-03-15', 320.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-1', 'initial deposit', null, null), (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, null), (uuid_generate_v4(), membership.uuid, 'DISBURSAL', '2021-09-01', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-2', 'partial disbursal', null),
(invalidLossTx, membership.uuid, 'DEPOSIT', '2022-10-20', 128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some loss', null, 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', invalidLossTx, null), (uuid_generate_v4(), membership.uuid, 'REVERSAL', '2022-10-21', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some reversal', lossEntryUuid);
(transferTx, membership.uuid, 'TRANSFER', '2023-12-31', -192.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some reversal', null, adoptionTx),
(adoptionTx, membership.uuid, 'ADOPTION', '2023-12-31', 192.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some reversal', null, null);
end; $$; end; $$;
--// --//

View File

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

View File

@ -0,0 +1,18 @@
--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

@ -0,0 +1,102 @@
--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,3 +171,9 @@ databaseChangeLog:
file: db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql file: db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql
- include: - include:
file: db/changelog/9-hs-global/9000-statistics.sql 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" "subscriber:customers-announce"
}; };
private static final String[] KNOWN_ROLES = ArrayUtils.addAll( private static final String[] KNOWN_ROLES = ArrayUtils.addAll(
new String[] { "partner", "vip-contact", "ex-partner", "billing", "contractual", "operation" }, new String[] { "partner", "vip-contact", "ex-partner", "billing", "contractual", "operation", "silent" },
SUBSCRIBER_ROLES); SUBSCRIBER_ROLES);
// at least as the number of lines in business_partners.csv from test-data, but less than real data partner count // at least as the number of lines in business_partners.csv from test-data, but less than real data partner count
@ -65,18 +65,21 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
static int relationId = INITIAL_RELATION_ID; static int relationId = INITIAL_RELATION_ID;
private static final List<Integer> IGNORE_BUSINESS_PARTNERS = Arrays.asList( 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 -1
); );
private static final List<Integer> IGNORE_CONTACTS = Arrays.asList( private static final List<Integer> IGNORE_CONTACTS = Arrays.asList(
90547, // Kontakt hat keine Rolle
-1 -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, HsOfficeContactRealEntity> contacts = new WriteOnceMap<>();
static Map<Integer, HsOfficePersonEntity> persons = new WriteOnceMap<>(); static Map<Integer, HsOfficePersonEntity> persons = new WriteOnceMap<>();
static Map<Integer, HsOfficePartnerEntity> partners = new WriteOnceMap<>(); static Map<Integer, HsOfficePartnerEntity> partners = new WriteOnceMap<>();
@ -192,56 +195,56 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
assertThat(toJsonFormattedString(partners)).isEqualToIgnoringWhitespace(""" assertThat(toJsonFormattedString(partners)).isEqualToIgnoringWhitespace("""
{ {
100=partner(P-10003: ?? Michael Mellis, Herr Michael Mellis , Michael Mellis), 100=partner(P-10003: ?? Michael Mellis, Herr Michael Mellis, Michael Mellis),
120=partner(P-10020: LP JM GmbH, Herr Philip Meyer-Contract , JM GmbH), 120=partner(P-10020: LP JM GmbH, Herr Philip Meyer-Contract, JM GmbH),
122=partner(P-11022: ?? Test PS, Petra Schmidt , Test PS), 122=partner(P-11022: ?? Test PS, Petra Schmidt, Test PS),
132=partner(P-10152: ?? Ragnar IT-Beratung, Herr Ragnar Richter , Ragnar IT-Beratung), 132=partner(P-10152: ?? Ragnar IT-Beratung, Herr Ragnar Richter, Ragnar IT-Beratung),
190=partner(P-19090: NP Camus, Cecilia, Frau Cecilia Camus ), 190=partner(P-19090: NP Camus, Cecilia, Frau Cecilia Camus),
199=partner(P-19999: null null, null), 199=partner(P-19999: null null, null),
213=partner(P-10000: LP Hostsharing e.G., Firma Hostmaster Hostsharing , Hostsharing e.G.), 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), 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) 542=partner(P-11019: ?? Das Perfekte Haus, Herr Richard Wiese, Das Perfekte Haus)
} }
"""); """);
assertThat(toJsonFormattedString(contacts)).isEqualToIgnoringWhitespace(""" 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"}'), 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"}'), 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"}'), 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"}'), 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"}'), 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"}'), 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"}'), 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"}'), 1401=contact(caption='Frau Frauke Fanninga', emailAddresses='{ "main": "ff@example.org"}'),
1501=contact(caption='Frau Cecilia Camus ', emailAddresses='{ "main": "cc@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"}'), 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"}'), 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"}'), 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"}'), 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"}'), 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"}'), 90629=contact(caption='Ragnar Richter', emailAddresses='{ "main": "mail@ragnar-richter..example.org"}'),
90677=contact(caption='Eike Henning ', emailAddresses='{ "main": "hostsharing@eike-henning..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"}') 90698=contact(caption='Jan Henning', emailAddresses='{ "main": "mail@jan-henning.example.org"}')
} }
"""); """);
assertThat(toJsonFormattedString(persons)).isEqualToIgnoringWhitespace(""" assertThat(toJsonFormattedString(persons)).isEqualToIgnoringWhitespace("""
{ {
100=person(personType='??', tradeName='Michael Mellis', familyName='Mellis', givenName='Michael'), 100=person(personType='??', tradeName='Michael Mellis', salutation='Herr', familyName='Mellis', givenName='Michael'),
1200=person(personType='LP', tradeName='JM e.K.'), 1200=person(personType='LP', tradeName='JM e.K.'),
1201=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Billing', givenName='Jenny'), 1201=person(personType='LP', tradeName='JM GmbH', salutation='Frau', title='Dr.', familyName='Meyer-Billing', givenName='Jenny'),
1202=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Operation', givenName='Andrew'), 1202=person(personType='LP', tradeName='JM GmbH', salutation='Herr', familyName='Meyer-Operation', givenName='Andrew'),
1203=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Contract', givenName='Philip'), 1203=person(personType='LP', tradeName='JM GmbH', salutation='Herr', familyName='Meyer-Contract', givenName='Philip'),
1204=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-VIP', givenName='Tammy'), 1204=person(personType='LP', tradeName='JM GmbH', salutation='Frau', familyName='Meyer-VIP', givenName='Tammy'),
1301=person(personType='??', tradeName='Test PS', familyName='Schmidt', givenName='Petra'), 1301=person(personType='??', tradeName='Test PS', familyName='Schmidt', givenName='Petra'),
132=person(personType='??', tradeName='Ragnar IT-Beratung', familyName='Richter', givenName='Ragnar'), 132=person(personType='??', tradeName='Ragnar IT-Beratung', salutation='Herr', familyName='Richter', givenName='Ragnar'),
1401=person(personType='NP', familyName='Fanninga', givenName='Frauke'), 1401=person(personType='NP', salutation='Frau', familyName='Fanninga', givenName='Frauke'),
1501=person(personType='NP', familyName='Camus', givenName='Cecilia'), 1501=person(personType='NP', salutation='Frau', familyName='Camus', givenName='Cecilia'),
212=person(personType='LP', tradeName='Hostsharing e.G.', familyName='Hostsharing', givenName='Hostmaster'), 212=person(personType='LP', tradeName='Hostsharing e.G.', salutation='Firma', familyName='Hostsharing', givenName='Hostmaster'),
90436=person(personType='??', tradeName='Wasserwerk Südholstein', familyName='Milberg', givenName='Christiane'), 90436=person(personType='??', tradeName='Wasserwerk Südholstein', salutation='Frau', familyName='Milberg', givenName='Christiane'),
90437=person(personType='??', tradeName='Das Perfekte Haus', familyName='Wiese', givenName='Richard'), 90437=person(personType='??', tradeName='Das Perfekte Haus', salutation='Herr', familyName='Wiese', givenName='Richard'),
90438=person(personType='??', tradeName='Wasswerwerk Südholstein', familyName='Metzger', givenName='Karim'), 90438=person(personType='??', tradeName='Wasswerwerk Südholstein', salutation='Herr', familyName='Metzger', givenName='Karim'),
90590=person(personType='??', tradeName='Das Perfekte Haus', familyName='Wiese', givenName='Inhaber R.'), 90590=person(personType='??', tradeName='Das Perfekte Haus', salutation='Herr', familyName='Wiese', givenName='Inhaber R.'),
90629=person(personType='NP', familyName='Richter', givenName='Ragnar'), 90629=person(personType='NP', familyName='Richter', givenName='Ragnar'),
90677=person(personType='NP', familyName='Henning', givenName='Eike'), 90677=person(personType='NP', familyName='Henning', givenName='Eike'),
90698=person(personType='NP', familyName='Henning', givenName='Jan') 90698=person(personType='NP', familyName='Henning', givenName='Jan')
@ -272,71 +275,81 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
"""); """);
assertThat(toJsonFormattedString(relations)).isEqualToIgnoringWhitespace(""" assertThat(toJsonFormattedString(relations)).isEqualToIgnoringWhitespace("""
{ {
2000000=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), 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'), 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'), 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'), 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.'), 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.'), 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'), 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'), 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'), 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'), 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'), 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'), 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'), 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'), 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 '), 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 '), 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'), 2000016=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='null null, null'),
2000017=rel(anchor='null null, null', type='DEBITOR'), 2000017=rel(anchor='null null, null', type='DEBITOR'),
2000018=rel(anchor='LP Hostsharing e.G.', type='OPERATIONS', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing , Hostsharing e.G.'), 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='REPRESENTATIVE', 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='?? Michael Mellis', type='OPERATIONS', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), 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='REPRESENTATIVE', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), 2000021=rel(anchor='?? Michael Mellis', type='OPERATIONS_ALERT', 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'), 2000022=rel(anchor='?? Michael Mellis', type='OPERATIONS', 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'), 2000023=rel(anchor='?? Michael Mellis', type='REPRESENTATIVE', 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'), 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='members-announce', 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='members-discussion', 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='?? Ragnar IT-Beratung', type='OPERATIONS', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'), 2000027=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='members-announce', holder='?? Michael Mellis', contact='Herr Michael Mellis, Michael Mellis'),
2000028=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-discussion', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'), 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='SUBSCRIBER', mark='operations-announce', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'), 2000029=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS_ALERT', 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.'), 2000030=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter, Ragnar IT-Beratung'),
2000031=rel(anchor='LP JM GmbH', type='OPERATIONS', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), 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='LP JM GmbH', type='VIP_CONTACT', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), 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='SUBSCRIBER', mark='operations-announce', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), 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='REPRESENTATIVE', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), 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='SUBSCRIBER', mark='members-announce', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , 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='SUBSCRIBER', mark='customers-announce', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , 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='VIP_CONTACT', holder='LP JM GmbH', contact='Frau Tammy Meyer-VIP , 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='?? Test PS', type='OPERATIONS', holder='?? Test PS', contact='Petra Schmidt , Test PS'), 2000038=rel(anchor='LP JM GmbH', type='REPRESENTATIVE', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract, JM GmbH'),
2000039=rel(anchor='?? Test PS', type='REPRESENTATIVE', holder='?? Test PS', contact='Petra Schmidt , Test PS'), 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='operations-announce', holder='NP Fanninga, Frauke', contact='Frau Frauke Fanninga '), 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='NP Camus, Cecilia', type='OPERATIONS', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '), 2000041=rel(anchor='LP JM GmbH', type='VIP_CONTACT', holder='LP JM GmbH', contact='Frau Tammy Meyer-VIP, JM GmbH'),
2000042=rel(anchor='NP Camus, Cecilia', type='REPRESENTATIVE', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '), 2000042=rel(anchor='?? Test PS', type='OPERATIONS_ALERT', holder='?? Test PS', contact='Petra Schmidt, Test PS'),
2000043=rel(anchor='?? Wasserwerk Südholstein', type='REPRESENTATIVE', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), 2000043=rel(anchor='?? Test PS', type='OPERATIONS', holder='?? Test PS', contact='Petra Schmidt, Test PS'),
2000044=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='generalversammlung', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), 2000044=rel(anchor='?? Test PS', type='REPRESENTATIVE', holder='?? Test PS', contact='Petra Schmidt, Test PS'),
2000045=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='members-announce', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), 2000045=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='operations-announce', holder='NP Fanninga, Frauke', contact='Frau Frauke Fanninga'),
2000046=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='members-discussion', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), 2000046=rel(anchor='NP Camus, Cecilia', type='OPERATIONS_ALERT', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus'),
2000047=rel(anchor='?? Das Perfekte Haus', type='OPERATIONS', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), 2000047=rel(anchor='NP Camus, Cecilia', type='OPERATIONS', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus'),
2000048=rel(anchor='?? Das Perfekte Haus', type='REPRESENTATIVE', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), 2000048=rel(anchor='NP Camus, Cecilia', type='REPRESENTATIVE', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus'),
2000049=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='operations-discussion', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), 2000049=rel(anchor='?? Wasserwerk Südholstein', type='REPRESENTATIVE', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg, Wasserwerk Südholstein'),
2000050=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='operations-announce', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), 2000050=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='generalversammlung', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg, Wasserwerk Südholstein'),
2000051=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='generalversammlung', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), 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='?? Das Perfekte Haus', type='SUBSCRIBER', mark='members-announce', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), 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='SUBSCRIBER', mark='members-discussion', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), 2000053=rel(anchor='?? Das Perfekte Haus', type='OPERATIONS_ALERT', 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'), 2000054=rel(anchor='?? Das Perfekte Haus', type='OPERATIONS', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese, Das Perfekte Haus'),
2000055=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='operations-discussion', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger , Wasswerwerk Südholstein'), 2000055=rel(anchor='?? Das Perfekte Haus', type='REPRESENTATIVE', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese, Das Perfekte Haus'),
2000056=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='operations-announce', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger , Wasswerwerk Südholstein'), 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='?? Ragnar IT-Beratung', type='REPRESENTATIVE', holder='NP Richter, Ragnar', contact='Ragnar Richter '), 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='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='generalversammlung', holder='NP Richter, Ragnar', contact='Ragnar Richter '), 2000058=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='generalversammlung', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese, Das Perfekte Haus'),
2000059=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='members-announce', holder='NP Richter, Ragnar', contact='Ragnar Richter '), 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='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='members-discussion', holder='NP Richter, Ragnar', contact='Ragnar Richter '), 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='?? Ragnar IT-Beratung', type='OPERATIONS', holder='NP Henning, Eike', contact='Eike Henning '), 2000061=rel(anchor='?? Wasserwerk Südholstein', type='OPERATIONS_ALERT', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger, Wasswerwerk Südholstein'),
2000062=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-discussion', holder='NP Henning, Eike', contact='Eike Henning '), 2000062=rel(anchor='?? Wasserwerk Südholstein', type='OPERATIONS', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger, Wasswerwerk Südholstein'),
2000063=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-announce', holder='NP Henning, Eike', contact='Eike Henning '), 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='?? Ragnar IT-Beratung', type='OPERATIONS', holder='NP Henning, Jan', contact='Jan Henning ') 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')
} }
"""); """);
} }
@ -436,7 +449,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
1094=CoopAssetsTransaction(M-1000300: 2023-10-06, DEPOSIT, 3072, 1000300, Kapitalerhoehung - Ueberweisung), 1094=CoopAssetsTransaction(M-1000300: 2023-10-06, DEPOSIT, 3072, 1000300, Kapitalerhoehung - Ueberweisung),
31000=CoopAssetsTransaction(M-1002000: 2000-12-06, DEPOSIT, 128.00, 1002000, for subscription B), 31000=CoopAssetsTransaction(M-1002000: 2000-12-06, DEPOSIT, 128.00, 1002000, for subscription B),
32000=CoopAssetsTransaction(M-1000300: 2005-01-10, DEPOSIT, 2560.00, 1000300, for subscription C), 32000=CoopAssetsTransaction(M-1000300: 2005-01-10, DEPOSIT, 2560.00, 1000300, for subscription C),
33001=CoopAssetsTransaction(M-1000300: 2005-01-10, TRANSFER, -512.00, 1000300, for transfer to 10, M-1002000:ADO:+512.00), 33001=CoopAssetsTransaction(M-1000300: 2005-01-10, TRANSFER, -512.00, 1000300, for transfer to 10),
33002=CoopAssetsTransaction(M-1002000: 2005-01-10, ADOPTION, 512.00, 1002000, for transfer from 7), 33002=CoopAssetsTransaction(M-1002000: 2005-01-10, ADOPTION, 512.00, 1002000, for transfer from 7),
34001=CoopAssetsTransaction(M-1002000: 2016-12-31, CLEARING, -8.00, 1002000, for cancellation D), 34001=CoopAssetsTransaction(M-1002000: 2016-12-31, CLEARING, -8.00, 1002000, for cancellation D),
34002=CoopAssetsTransaction(M-1002000: 2016-12-31, DISBURSAL, -100.00, 1002000, for cancellation D), 34002=CoopAssetsTransaction(M-1002000: 2016-12-31, DISBURSAL, -100.00, 1002000, for cancellation D),
@ -502,7 +515,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
// this happens if a natural person is marked as 'contractual' for itself // this happens if a natural person is marked as 'contractual' for itself
final var idsToRemove = new HashSet<Integer>(); final var idsToRemove = new HashSet<Integer>();
relations.forEach((id, r) -> { relations.forEach((id, r) -> {
if (r.getHolder() == r.getAnchor()) { if (r.getType() == HsOfficeRelationType.REPRESENTATIVE && r.getHolder() == r.getAnchor()) {
idsToRemove.add(id); idsToRemove.add(id);
} }
}); });
@ -670,7 +683,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
jpaAttempt.transacted(() -> { jpaAttempt.transacted(() -> {
context(rbacSuperuser); context(rbacSuperuser);
coopShares.forEach(this::persist); coopShares.forEach(this::persist);
updateLegacyIds(coopShares, "hs_office.coopsharestransaction_legacy_id", "member_share_id"); updateLegacyIds(coopShares, "hs_office.coopsharetx_legacy_id", "member_share_id");
}).assertSuccessful(); }).assertSuccessful();
@ -864,20 +877,8 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
.comment(rec.getString("comment")) .comment(rec.getString("comment"))
.reference(member.getMemberNumber().toString()) .reference(member.getMemberNumber().toString())
.build(); .build();
coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction);
});
coopAssets.values().forEach(assetTransaction -> {
if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.REVERSAL) { 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 negativeValue = assetTransaction.getAssetValue().negate();
final var revertedAssetTx = coopAssets.values().stream().filter(a -> final var revertedAssetTx = coopAssets.values().stream().filter(a ->
a.getTransactionType() != HsOfficeCoopAssetsTransactionType.REVERSAL && a.getTransactionType() != HsOfficeCoopAssetsTransactionType.REVERSAL &&
@ -887,21 +888,10 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
.orElseThrow(() -> new IllegalStateException( .orElseThrow(() -> new IllegalStateException(
"cannot determine asset reverse entry for reversal " + assetTransaction)); "cannot determine asset reverse entry for reversal " + assetTransaction));
assetTransaction.setRevertedAssetTx(revertedAssetTx); assetTransaction.setRevertedAssetTx(revertedAssetTx);
//revertedAssetTx.setAssetReversalTx(assetTransaction);
} }
private static void connectToRelatedAdoptionAssetTx(final HsOfficeCoopAssetsTransactionEntity assetTransaction) { coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction);
final var negativeValue = assetTransaction.getAssetValue().negate(); });
final var adoptionAssetTx = coopAssets.values().stream().filter(a ->
a.getTransactionType() == HsOfficeCoopAssetsTransactionType.ADOPTION &&
a.getMembership() != assetTransaction.getMembership() &&
a.getValueDate().equals(assetTransaction.getValueDate()) &&
a.getAssetValue().equals(negativeValue))
.findAny()
.orElseThrow(() -> new IllegalStateException(
"cannot determine asset adoption entry for reversal " + assetTransaction));
assetTransaction.setAdoptionAssetTx(adoptionAssetTx);
//adoptionAssetTx.setAssetTransferTx(assetTransaction);
} }
private static HsOfficeMembershipEntity createOnDemandMembership(final Integer bpId) { private static HsOfficeMembershipEntity createOnDemandMembership(final Integer bpId) {
@ -981,6 +971,9 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
HsOfficePersonEntity contactPerson = partnerPerson; HsOfficePersonEntity contactPerson = partnerPerson;
if (!StringUtils.equals(rec.getString("firma"), partnerPerson.getTradeName()) || 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("first_name"), partnerPerson.getGivenName()) ||
!StringUtils.equals(rec.getString("last_name"), partnerPerson.getFamilyName())) { !StringUtils.equals(rec.getString("last_name"), partnerPerson.getFamilyName())) {
contactPerson = addPerson(HsOfficePersonEntity.builder().build(), rec); contactPerson = addPerson(HsOfficePersonEntity.builder().build(), rec);
@ -999,6 +992,10 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
debitor.getDebitorRel().setContact(contact); debitor.getDebitorRel().setContact(contact);
} }
if (containsRole(rec, "operation")) { 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); addRelation(HsOfficeRelationType.OPERATIONS, partnerPerson, contactPerson, contact);
} }
if (containsRole(rec, "contractual")) { if (containsRole(rec, "contractual")) {
@ -1076,34 +1073,60 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
} }
private HsOfficePersonEntity addPerson(final HsOfficePersonEntity person, final Record contactRecord) { private HsOfficePersonEntity addPerson(final HsOfficePersonEntity person, final Record contactRecord) {
// TODO: title+salutation: add to person person.setSalutation(contactRecord.getString("salut"));
person.setTitle(contactRecord.getString("title"));
person.setGivenName(contactRecord.getString("first_name")); person.setGivenName(contactRecord.getString("first_name"));
person.setFamilyName(contactRecord.getString("last_name")); person.setFamilyName(contactRecord.getString("last_name"));
person.setTradeName(contactRecord.getString("firma")); person.setTradeName(contactRecord.getString("firma"));
determinePersonType(person, contactRecord.getString("roles")); person.setPersonType(determinePersonType(contactRecord));
persons.put(contactRecord.getInteger("contact_id"), person); persons.put(contactRecord.getInteger("contact_id"), person);
return person; return person;
} }
private static void determinePersonType(final HsOfficePersonEntity person, final String roles) { private static HsOfficePersonType determinePersonType(final Record contactRecord) {
if (person.getTradeName().isBlank()) { String roles = contactRecord.getString("roles");
person.setPersonType(HsOfficePersonType.NATURAL_PERSON); 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;
} else } else
// contractual && !partner with a firm and a natural person name // contractual && !partner with a firm and a natural person name
// should actually be split up into two persons // should actually be split up into two persons
// but the legacy database consists such records // but the legacy database consists such records
if (roles.contains("contractual") && !roles.contains("partner") &&
!person.getFamilyName().isBlank() && !person.getGivenName().isBlank()) { if (endsWithWord(tradeName, "OHG", "GbR", "KG", "UG", "PartGmbB", "mbB")) {
person.setPersonType(HsOfficePersonType.NATURAL_PERSON); return HsOfficePersonType.INCORPORATED_FIRM; // Personengesellschaft. Gesellschafter haften persönlich.
} else if (endsWithWord(person.getTradeName(), "e.K.", "e.G.", "eG", "GmbH", "AG", "KG")) { } else if (containsWord(tradeName, "e.K.", "e.G.", "eG", "gGmbH", "GmbH", "mbH", "AG", "e.V.", "eV", "e.V")
person.setPersonType(HsOfficePersonType.LEGAL_PERSON); || tradeName.toLowerCase().contains("haftungsbeschränkt")
} else if (endsWithWord(person.getTradeName(), "OHG")) { || tradeName.toLowerCase().contains("stiftung")
person.setPersonType(HsOfficePersonType.INCORPORATED_FIRM); || tradeName.toLowerCase().contains("stichting")
} else if (endsWithWord(person.getTradeName(), "GbR")) { || tradeName.toLowerCase().contains("foundation")
person.setPersonType(HsOfficePersonType.INCORPORATED_FIRM); || 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;
} else { } else {
person.setPersonType(HsOfficePersonType.UNKNOWN_PERSON_TYPE); return HsOfficePersonType.UNKNOWN_PERSON_TYPE;
} }
} }
@ -1117,6 +1140,19 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
return false; 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) { private void verifyContainsOnlyKnownRoles(final String roles) {
final var allowedRolesSet = stream(KNOWN_ROLES).collect(Collectors.toSet()); final var allowedRolesSet = stream(KNOWN_ROLES).collect(Collectors.toSet());
final var givenRolesSet = stream(roles.replace(" ", "").split(",")).collect(Collectors.toSet()); final var givenRolesSet = stream(roles.replace(" ", "").split(",")).collect(Collectors.toSet());
@ -1181,13 +1217,13 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
final String firm) { final String firm) {
final var result = new StringBuilder(); final var result = new StringBuilder();
if (isNotBlank(salut)) if (isNotBlank(salut))
result.append(salut + " "); result.append((isBlank(result) ? "" : " ") + salut);
if (isNotBlank(title)) if (isNotBlank(title))
result.append(title + " "); result.append((isBlank(result) ? "" : " ") + title);
if (isNotBlank(firstname)) if (isNotBlank(firstname))
result.append(firstname + " "); result.append((isBlank(result) ? "" : " ") + firstname);
if (isNotBlank(lastname)) if (isNotBlank(lastname))
result.append(lastname + " "); result.append((isBlank(result) ? "" : " ") + lastname);
if (isNotBlank(firm)) { if (isNotBlank(firm)) {
result.append((isBlank(result) ? "" : ", ") + firm); result.append((isBlank(result) ? "" : ", ") + firm);
} }

View File

@ -173,13 +173,8 @@ public class CsvDataImport extends ContextBasedTest {
//System.out.println("persisting #" + entity.hashCode() + ": " + entity); //System.out.println("persisting #" + entity.hashCode() + ": " + entity);
em.persist(entity); em.persist(entity);
// uncomment for debugging purposes // uncomment for debugging purposes
// try {
// em.flush(); // makes it slow, but produces better error messages // em.flush(); // makes it slow, but produces better error messages
// System.out.println("persisted #" + entity.hashCode() + " as " + entity.getUuid()); // System.out.println("persisted #" + entity.hashCode() + " as " + entity.getUuid());
// return entity;
// } catch (final Exception exc) {
// throw exc; // for breakpoints
// }
return entity; return entity;
} }
@ -260,7 +255,7 @@ public class CsvDataImport extends ContextBasedTest {
em.createNativeQuery("delete from hs_office.coopassettx where true").executeUpdate(); 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.coopassettx_legacy_id where true").executeUpdate();
em.createNativeQuery("delete from hs_office.coopsharetx where true").executeUpdate(); em.createNativeQuery("delete from hs_office.coopsharetx where true").executeUpdate();
em.createNativeQuery("delete from hs_office.coopsharestransaction_legacy_id where true").executeUpdate(); em.createNativeQuery("delete from hs_office.coopsharetx_legacy_id where true").executeUpdate();
em.createNativeQuery("delete from hs_office.membership 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 where true").executeUpdate();
em.createNativeQuery("delete from hs_office.sepamandate_legacy_id where true").executeUpdate(); em.createNativeQuery("delete from hs_office.sepamandate_legacy_id where true").executeUpdate();
@ -280,7 +275,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.contact_legacy_id_seq restart with 1000000000;").executeUpdate();
em.createNativeQuery("alter sequence hs_office.coopassettx_legacy_id_seq restart with 1000000000;") em.createNativeQuery("alter sequence hs_office.coopassettx_legacy_id_seq restart with 1000000000;")
.executeUpdate(); .executeUpdate();
em.createNativeQuery("alter sequence public.hs_office.coopsharestransaction_legacy_id_seq restart with 1000000000;") em.createNativeQuery("alter sequence public.hs_office.coopsharetx_legacy_id_seq restart with 1000000000;")
.executeUpdate(); .executeUpdate();
em.createNativeQuery("alter sequence public.hs_office.partner_legacy_id_seq restart with 1000000000;") em.createNativeQuery("alter sequence public.hs_office.partner_legacy_id_seq restart with 1000000000;")
.executeUpdate(); .executeUpdate();

View File

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

View File

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

View File

@ -69,7 +69,7 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
.then().log().all().assertThat() .then().log().all().assertThat()
.statusCode(200) .statusCode(200)
.contentType("application/json") .contentType("application/json")
.body("", hasSize(3*6)); // @formatter:on .body("", hasSize(12)); // @formatter:on
} }
@Test @Test
@ -94,22 +94,14 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
"assetValue": 320.00, "assetValue": 320.00,
"valueDate": "2010-03-15", "valueDate": "2010-03-15",
"reference": "ref 1000202-1", "reference": "ref 1000202-1",
"comment": "initial deposit", "comment": "initial deposit"
"adoptionAssetTx": null,
"transferAssetTx": null,
"revertedAssetTx": null,
"reversalAssetTx": null
}, },
{ {
"transactionType": "DISBURSAL", "transactionType": "DISBURSAL",
"assetValue": -128.00, "assetValue": -128.00,
"valueDate": "2021-09-01", "valueDate": "2021-09-01",
"reference": "ref 1000202-2", "reference": "ref 1000202-2",
"comment": "partial disbursal", "comment": "partial disbursal"
"adoptionAssetTx": null,
"transferAssetTx": null,
"revertedAssetTx": null,
"reversalAssetTx": null
}, },
{ {
"transactionType": "DEPOSIT", "transactionType": "DEPOSIT",
@ -117,18 +109,12 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
"valueDate": "2022-10-20", "valueDate": "2022-10-20",
"reference": "ref 1000202-3", "reference": "ref 1000202-3",
"comment": "some loss", "comment": "some loss",
"adoptionAssetTx": null,
"transferAssetTx": null,
"revertedAssetTx": null,
"reversalAssetTx": { "reversalAssetTx": {
"transactionType": "REVERSAL", "transactionType": "REVERSAL",
"assetValue": -128.00, "assetValue": -128.00,
"valueDate": "2022-10-21", "valueDate": "2022-10-21",
"reference": "ref 1000202-3", "reference": "ref 1000202-3",
"comment": "some reversal", "comment": "some reversal"
"adoptionAssetTx.uuid": null,
"transferAssetTx.uuid": null,
"reversalAssetTx.uuid": null
} }
}, },
{ {
@ -137,59 +123,13 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
"valueDate": "2022-10-21", "valueDate": "2022-10-21",
"reference": "ref 1000202-3", "reference": "ref 1000202-3",
"comment": "some reversal", "comment": "some reversal",
"adoptionAssetTx": null,
"transferAssetTx": null,
"revertedAssetTx": { "revertedAssetTx": {
"transactionType": "DEPOSIT", "transactionType": "DEPOSIT",
"assetValue": 128.00, "assetValue": 128.00,
"valueDate": "2022-10-20", "valueDate": "2022-10-20",
"reference": "ref 1000202-3", "reference": "ref 1000202-3",
"comment": "some loss", "comment": "some loss"
"adoptionAssetTx.uuid": null, }
"transferAssetTx.uuid": null,
"revertedAssetTx.uuid": null
},
"reversalAssetTx": null
},
{
"transactionType": "TRANSFER",
"assetValue": -192.00,
"valueDate": "2023-12-31",
"reference": "ref 1000202-3",
"comment": "some reversal",
"adoptionAssetTx": {
"transactionType": "ADOPTION",
"assetValue": 192.00,
"valueDate": "2023-12-31",
"reference": "ref 1000202-3",
"comment": "some reversal",
"adoptionAssetTx.uuid": null,
"revertedAssetTx.uuid": null,
"reversalAssetTx.uuid": null
},
"transferAssetTx": null,
"revertedAssetTx": null,
"reversalAssetTx": null
},
{
"transactionType": "ADOPTION",
"assetValue": 192.00,
"valueDate": "2023-12-31",
"reference": "ref 1000202-3",
"comment": "some reversal",
"adoptionAssetTx": null,
"transferAssetTx": {
"transactionType": "TRANSFER",
"assetValue": -192.00,
"valueDate": "2023-12-31",
"reference": "ref 1000202-3",
"comment": "some reversal",
"transferAssetTx.uuid": null,
"revertedAssetTx.uuid": null,
"reversalAssetTx.uuid": null
},
"revertedAssetTx": null,
"reversalAssetTx": null
} }
] ]
""")); // @formatter:on """)); // @formatter:on

View File

@ -1,116 +1,50 @@
package net.hostsharing.hsadminng.hs.office.coopassets; package net.hostsharing.hsadminng.hs.office.coopassets;
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import net.hostsharing.hsadminng.rbac.test.JsonBuilder; import net.hostsharing.hsadminng.rbac.test.JsonBuilder;
import net.hostsharing.hsadminng.test.TestUuidGenerator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.EnumSource;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.function.Function; import java.util.function.Function;
import static net.hostsharing.hsadminng.rbac.test.JsonBuilder.jsonObject; import static net.hostsharing.hsadminng.rbac.test.JsonBuilder.jsonObject;
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HsOfficeCoopAssetsTransactionController.class) @WebMvcTest(HsOfficeCoopAssetsTransactionController.class)
@Import({ StrictMapper.class, JsonObjectMapperConfiguration.class })
@RunWith(SpringRunner.class)
class HsOfficeCoopAssetsTransactionControllerRestTest { class HsOfficeCoopAssetsTransactionControllerRestTest {
private static final UUID UNAVAILABLE_MEMBERSHIP_UUID = TestUuidGenerator.use(0);
private static final String UNAVAILABLE_MEMBER_NUMBER = "M-1234699";
private static final UUID ORIGIN_MEMBERSHIP_UUID = TestUuidGenerator.use(1);
private static final String ORIGIN_MEMBER_NUMBER = "M-1111100";
public final HsOfficeMembershipEntity ORIGIN_TARGET_MEMBER_ENTITY = HsOfficeMembershipEntity.builder()
.uuid(ORIGIN_MEMBERSHIP_UUID)
.partner(HsOfficePartnerEntity.builder()
.partnerNumber(partnerNumberOf(ORIGIN_MEMBER_NUMBER))
.build())
.memberNumberSuffix(suffixOf(ORIGIN_MEMBER_NUMBER))
.build();
private static final UUID AVAILABLE_TARGET_MEMBERSHIP_UUID = TestUuidGenerator.use(2);
private static final String AVAILABLE_TARGET_MEMBER_NUMBER = "M-1234500";
public final HsOfficeMembershipEntity AVAILABLE_MEMBER_ENTITY = HsOfficeMembershipEntity.builder()
.uuid(AVAILABLE_TARGET_MEMBERSHIP_UUID)
.partner(HsOfficePartnerEntity.builder()
.partnerNumber(partnerNumberOf(AVAILABLE_TARGET_MEMBER_NUMBER))
.build())
.memberNumberSuffix(suffixOf(AVAILABLE_TARGET_MEMBER_NUMBER))
.build();
// the following refs might change if impl changes
private static final UUID NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID = TestUuidGenerator.ref(4);
private static final UUID NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID = TestUuidGenerator.ref(5);
private static final UUID SOME_EXISTING_LOSS_ASSET_TX_UUID = TestUuidGenerator.use(3);
public final HsOfficeCoopAssetsTransactionEntity SOME_EXISTING_LOSS_ASSET_TX_ENTITY = HsOfficeCoopAssetsTransactionEntity.builder()
.uuid(SOME_EXISTING_LOSS_ASSET_TX_UUID)
.membership(ORIGIN_TARGET_MEMBER_ENTITY)
.transactionType(HsOfficeCoopAssetsTransactionType.LOSS)
.assetValue(BigDecimal.valueOf(-64))
.reference("some loss asset tx ref")
.comment("some loss asset tx comment")
.valueDate(LocalDate.parse("2024-10-15"))
.build();
@Autowired @Autowired
MockMvc mockMvc; MockMvc mockMvc;
@MockBean @MockBean
Context contextMock; Context contextMock;
@Autowired
@SuppressWarnings("unused") // not used in test, but in controller class
StrictMapper mapper;
@MockBean @MockBean
EntityManagerWrapper emw; // even if not used in test anymore, it's needed by base-class of StrictMapper StandardMapper mapper;
@MockBean @MockBean
HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo; HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo;
@MockBean static final String VALID_INSERT_REQUEST_BODY = """
HsOfficeMembershipRepository membershipRepo;
static final String INSERT_REQUEST_BODY_TEMPLATE = """
{ {
"membership.uuid": "%s", "membership.uuid": "%s",
"transactionType": "DEPOSIT", "transactionType": "DEPOSIT",
"assetValue": 128.00, "assetValue": 128.00,
"valueDate": "2022-10-13", "valueDate": "2022-10-13",
"reference": "valid reference", "reference": "valid reference",
"comment": "valid comment", "comment": "valid comment"
"adoptingMembership.uuid": null,
"adoptingMembership.memberNumber": null
} }
""".formatted(ORIGIN_MEMBERSHIP_UUID); """.formatted(UUID.randomUUID());
enum BadRequestTestCases { enum BadRequestTestCases {
MEMBERSHIP_UUID_MISSING( MEMBERSHIP_UUID_MISSING(
@ -131,6 +65,8 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
.with("assetValue", -64.00), .with("assetValue", -64.00),
"[for DEPOSIT, assetValue must be positive but is \"-64.00\"]"), "[for DEPOSIT, assetValue must be positive but is \"-64.00\"]"),
//TODO: other transaction types
ASSETS_VALUE_FOR_DISBURSAL_MUST_BE_NEGATIVE( ASSETS_VALUE_FOR_DISBURSAL_MUST_BE_NEGATIVE(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "DISBURSAL") .with("transactionType", "DISBURSAL")
@ -139,20 +75,6 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
//TODO: other transaction types //TODO: other transaction types
ADOPTING_MEMBERSHIP_NUMBER_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE(
requestBody -> requestBody
.with("transactionType", "TRANSFER")
.with("assetValue", -64.00)
.with("adoptingMembership.memberNumber", UNAVAILABLE_MEMBER_NUMBER),
"adoptingMembership.memberNumber='M-1234699' not found or not accessible"),
ADOPTING_MEMBERSHIP_UUID_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE(
requestBody -> requestBody
.with("transactionType", "TRANSFER")
.with("assetValue", -64.00)
.with("adoptingMembership.uuid", UNAVAILABLE_MEMBERSHIP_UUID.toString()),
"adoptingMembership.uuid='" + UNAVAILABLE_MEMBERSHIP_UUID + "' not found or not accessible"),
ASSETS_VALUE_MUST_NOT_BE_NULL( ASSETS_VALUE_MUST_NOT_BE_NULL(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "REVERSAL") .with("transactionType", "REVERSAL")
@ -182,16 +104,13 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
} }
String givenRequestBody() { String givenRequestBody() {
return givenBodyTransformation.apply(jsonObject(INSERT_REQUEST_BODY_TEMPLATE)).toString(); return givenBodyTransformation.apply(jsonObject(VALID_INSERT_REQUEST_BODY)).toString();
} }
} }
@ParameterizedTest @ParameterizedTest
@EnumSource(BadRequestTestCases.class) @EnumSource(BadRequestTestCases.class)
void respondWithBadRequest(final BadRequestTestCases testCase) throws Exception { void respondWithBadRequest(final BadRequestTestCases testCase) throws Exception {
// HOWTO: run just a single test-case in a data-driven test-method
// org.assertj.core.api.Assumptions.assumeThat(
// testCase == ADOPTING_MEMBERSHIP_NUMBER_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE).isTrue();
// when // when
mockMvc.perform(MockMvcRequestBuilders mockMvc.perform(MockMvcRequestBuilders
@ -208,160 +127,4 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
.andExpect(status().is4xxClientError()); .andExpect(status().is4xxClientError());
} }
enum SuccessfullyCreatedTestCases {
REVERTING_SIMPLE_ASSET_TRANSACTION(
requestBody -> requestBody
.with("transactionType", "REVERSAL")
.with("assetValue", "64.00")
.with("valueDate", "2024-10-15")
.with("reference", "reversal ref")
.with("comment", "reversal comment")
.with("revertedAssetTx.uuid", SOME_EXISTING_LOSS_ASSET_TX_UUID.toString()),
Expected.REVERT_RESPONSE),
TRANSFER_TO_GIVEN_AVAILABLE_MEMBERSHIP_NUMBER(
requestBody -> requestBody
.with("transactionType", "TRANSFER")
.with("assetValue", -64.00)
.with("adoptingMembership.memberNumber", AVAILABLE_TARGET_MEMBER_NUMBER),
Expected.TRANSFER_RESPONSE),
TRANSFER_TO_GIVEN_AVAILABLE_MEMBERSHIP_UUID(
requestBody -> requestBody
.with("transactionType", "TRANSFER")
.with("assetValue", -64.00)
.with("membership.uuid", ORIGIN_MEMBERSHIP_UUID.toString())
.with("adoptingMembership.uuid", AVAILABLE_TARGET_MEMBERSHIP_UUID.toString()),
Expected.TRANSFER_RESPONSE);
private final Function<JsonBuilder, JsonBuilder> givenBodyTransformation;
private final String expectedResponseBody;
SuccessfullyCreatedTestCases(
final Function<JsonBuilder, JsonBuilder> givenBodyTransformation,
final String expectedResponseBody) {
this.givenBodyTransformation = givenBodyTransformation;
this.expectedResponseBody = expectedResponseBody;
}
String givenRequestBody() {
return givenBodyTransformation.apply(jsonObject(INSERT_REQUEST_BODY_TEMPLATE)).toString();
}
private static class Expected {
public static final String REVERT_RESPONSE = """
{
"uuid": "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}",
"membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}",
"membership.memberNumber": "%{ORIGIN_MEMBER_NUMBER}",
"transactionType": "REVERSAL",
"assetValue": 64.00,
"valueDate": "2024-10-15",
"reference": "reversal ref",
"comment": "reversal comment",
"adoptionAssetTx": null,
"transferAssetTx": null,
"revertedAssetTx": {
"uuid": "%{SOME_EXISTING_LOSS_ASSET_TX_UUID}",
"membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}",
"membership.memberNumber": "%{ORIGIN_MEMBER_NUMBER}",
"transactionType": "LOSS",
"assetValue": -64.00,
"valueDate": "2024-10-15",
"reference": "some loss asset tx ref",
"comment": "some loss asset tx comment",
"adoptionAssetTx.uuid": null,
"transferAssetTx.uuid": null,
"revertedAssetTx.uuid": null,
"reversalAssetTx.uuid": "%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}"
}
}
"""
.replace("%{NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID}", NEW_EXPLICITLY_CREATED_REVERSAL_ASSET_TX_UUID.toString())
.replace("%{ORIGIN_MEMBERSHIP_UUID}", ORIGIN_MEMBERSHIP_UUID.toString())
.replace("%{ORIGIN_MEMBER_NUMBER}", ORIGIN_MEMBER_NUMBER)
.replace("%{SOME_EXISTING_LOSS_ASSET_TX_UUID}", SOME_EXISTING_LOSS_ASSET_TX_UUID.toString());
public static final String TRANSFER_RESPONSE = """
{
"uuid": "%{NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID}",
"membership.uuid": "%{ORIGIN_MEMBERSHIP_UUID}",
"membership.memberNumber": "%{ORIGIN_MEMBER_NUMBER}",
"transactionType": "TRANSFER",
"assetValue": -64.00,
"adoptionAssetTx": {
"membership.uuid": "%{AVAILABLE_MEMBERSHIP_UUID}",
"membership.memberNumber": "%{AVAILABLE_TARGET_MEMBER_NUMBER}",
"transactionType": "ADOPTION",
"assetValue": 64.00,
"transferAssetTx.uuid": "%{NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID}"
},
"transferAssetTx": null,
"revertedAssetTx": null,
"reversalAssetTx": null
}
"""
.replace("%{NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID}", NEW_EXPLICITLY_CREATED_TRANSFER_ASSET_TX_UUID.toString())
.replace("%{ORIGIN_MEMBERSHIP_UUID}", ORIGIN_MEMBERSHIP_UUID.toString())
.replace("%{ORIGIN_MEMBER_NUMBER}", ORIGIN_MEMBER_NUMBER)
.replace("%{AVAILABLE_MEMBERSHIP_UUID}", AVAILABLE_TARGET_MEMBERSHIP_UUID.toString())
.replace("%{AVAILABLE_TARGET_MEMBER_NUMBER}", AVAILABLE_TARGET_MEMBER_NUMBER);
}
}
@ParameterizedTest
@EnumSource(SuccessfullyCreatedTestCases.class)
void respondWithSuccessfullyCreated(final SuccessfullyCreatedTestCases testCase) throws Exception {
// uncomment, if you need to run just a single test-case in this data-driven test-method
// org.assertj.core.api.Assumptions.assumeThat(
// testCase == ADOPTING_MEMBERSHIP_UUID_FOR_TRANSFER_MUST_BE_GIVEN_AND_AVAILABLE).isTrue();
// when
mockMvc.perform(MockMvcRequestBuilders
.post("/api/hs/office/coopassetstransactions")
.header("current-subject", "superuser-alex@hostsharing.net")
.contentType(MediaType.APPLICATION_JSON)
.content(testCase.givenRequestBody())
.accept(MediaType.APPLICATION_JSON))
// then
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$", lenientlyEquals(testCase.expectedResponseBody)));
}
@BeforeEach
void initMocks() {
TestUuidGenerator.start(4);
when(emw.find(eq(HsOfficeMembershipEntity.class), eq(ORIGIN_MEMBERSHIP_UUID))).thenReturn(ORIGIN_TARGET_MEMBER_ENTITY);
when(emw.find(eq(HsOfficeMembershipEntity.class), eq(AVAILABLE_TARGET_MEMBERSHIP_UUID))).thenReturn(AVAILABLE_MEMBER_ENTITY);
final var availableMemberNumber = Integer.valueOf(AVAILABLE_TARGET_MEMBER_NUMBER.substring("M-".length()));
when(membershipRepo.findMembershipByMemberNumber(eq(availableMemberNumber))).thenReturn(AVAILABLE_MEMBER_ENTITY);
when(membershipRepo.findByUuid(eq(ORIGIN_MEMBERSHIP_UUID))).thenReturn(Optional.of(ORIGIN_TARGET_MEMBER_ENTITY));
when(membershipRepo.findByUuid(eq(AVAILABLE_TARGET_MEMBERSHIP_UUID))).thenReturn(Optional.of(AVAILABLE_MEMBER_ENTITY));
when(coopAssetsTransactionRepo.findByUuid(SOME_EXISTING_LOSS_ASSET_TX_UUID))
.thenReturn(Optional.of(SOME_EXISTING_LOSS_ASSET_TX_ENTITY));
when(coopAssetsTransactionRepo.save(any(HsOfficeCoopAssetsTransactionEntity.class)))
.thenAnswer(invocation -> {
final var entity = (HsOfficeCoopAssetsTransactionEntity) invocation.getArgument(0);
if (entity.getUuid() == null) {
entity.setUuid(TestUuidGenerator.next());
}
return entity;
}
);
}
private int partnerNumberOf(final String memberNumber) {
return Integer.parseInt(memberNumber.substring("M-".length(), memberNumber.length()-2));
}
private String suffixOf(final String memberNumber) {
return memberNumber.substring("M-".length()+5);
}
} }

View File

@ -20,6 +20,7 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest {
.comment("some comment") .comment("some comment")
.build(); .build();
final HsOfficeCoopAssetsTransactionEntity givenCoopAssetReversalTransaction = HsOfficeCoopAssetsTransactionEntity.builder() final HsOfficeCoopAssetsTransactionEntity givenCoopAssetReversalTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
.membership(TEST_MEMBERSHIP) .membership(TEST_MEMBERSHIP)
.reference("some-ref") .reference("some-ref")
@ -30,16 +31,6 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest {
.revertedAssetTx(givenCoopAssetTransaction) .revertedAssetTx(givenCoopAssetTransaction)
.build(); .build();
final HsOfficeCoopAssetsTransactionEntity givenAdoptedCoopAssetTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
.membership(TEST_MEMBERSHIP)
.reference("some-ref")
.valueDate(LocalDate.parse("2020-01-15"))
.transactionType(HsOfficeCoopAssetsTransactionType.ADOPTION)
.assetValue(new BigDecimal("128.00"))
.comment("some comment")
.revertedAssetTx(givenCoopAssetTransaction)
.build();
final HsOfficeCoopAssetsTransactionEntity givenEmptyCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder().build(); final HsOfficeCoopAssetsTransactionEntity givenEmptyCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder().build();
@Test @Test
@ -58,15 +49,6 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest {
assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref, some comment, M-1000101:REV:-128.00)"); assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref, some comment, M-1000101:REV:-128.00)");
} }
@Test
void toStringWithAdoptedAssetTxContainsRevertedAssetTx() {
givenCoopAssetTransaction.setAdoptionAssetTx(givenAdoptedCoopAssetTransaction);
final var result = givenCoopAssetTransaction.toString();
assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref, some comment, M-1000101:ADO:+128.00)");
}
@Test @Test
void toShortStringContainsOnlyMemberNumberSuffixAndSharesCountOnly() { void toShortStringContainsOnlyMemberNumberSuffixAndSharesCountOnly() {
final var result = givenCoopAssetTransaction.toShortString(); final var result = givenCoopAssetTransaction.toShortString();

View File

@ -144,22 +144,16 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
"CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)", "CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)",
"CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:REV:-128.00)", "CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:REV:-128.00)",
"CoopAssetsTransaction(M-1000101: 2022-10-21, REVERSAL, -128.00, ref 1000101-3, some reversal, M-1000101:DEP:+128.00)", "CoopAssetsTransaction(M-1000101: 2022-10-21, REVERSAL, -128.00, ref 1000101-3, some reversal, M-1000101:DEP:+128.00)",
"CoopAssetsTransaction(M-1000101: 2023-12-31, ADOPTION, 192.00, ref 1000101-3, some reversal, M-1000101:TRA:-192.00)",
"CoopAssetsTransaction(M-1000101: 2023-12-31, TRANSFER, -192.00, ref 1000101-3, some reversal, M-1000101:ADO:+192.00)",
"CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)", "CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)",
"CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)", "CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)",
"CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:REV:-128.00)", "CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:REV:-128.00)",
"CoopAssetsTransaction(M-1000202: 2022-10-21, REVERSAL, -128.00, ref 1000202-3, some reversal, M-1000202:DEP:+128.00)", "CoopAssetsTransaction(M-1000202: 2022-10-21, REVERSAL, -128.00, ref 1000202-3, some reversal, M-1000202:DEP:+128.00)",
"CoopAssetsTransaction(M-1000202: 2023-12-31, TRANSFER, -192.00, ref 1000202-3, some reversal, M-1000202:ADO:+192.00)",
"CoopAssetsTransaction(M-1000202: 2023-12-31, ADOPTION, 192.00, ref 1000202-3, some reversal, M-1000202:TRA:-192.00)",
"CoopAssetsTransaction(M-1000303: 2010-03-15, DEPOSIT, 320.00, ref 1000303-1, initial deposit)", "CoopAssetsTransaction(M-1000303: 2010-03-15, DEPOSIT, 320.00, ref 1000303-1, initial deposit)",
"CoopAssetsTransaction(M-1000303: 2021-09-01, DISBURSAL, -128.00, ref 1000303-2, partial disbursal)", "CoopAssetsTransaction(M-1000303: 2021-09-01, DISBURSAL, -128.00, ref 1000303-2, partial disbursal)",
"CoopAssetsTransaction(M-1000303: 2022-10-20, DEPOSIT, 128.00, ref 1000303-3, some loss, M-1000303:REV:-128.00)", "CoopAssetsTransaction(M-1000303: 2022-10-20, DEPOSIT, 128.00, ref 1000303-3, some loss, M-1000303:REV:-128.00)",
"CoopAssetsTransaction(M-1000303: 2022-10-21, REVERSAL, -128.00, ref 1000303-3, some reversal, M-1000303:DEP:+128.00)", "CoopAssetsTransaction(M-1000303: 2022-10-21, REVERSAL, -128.00, ref 1000303-3, some reversal, M-1000303:DEP:+128.00)");
"CoopAssetsTransaction(M-1000303: 2023-12-31, TRANSFER, -192.00, ref 1000303-3, some reversal, M-1000303:ADO:+192.00)",
"CoopAssetsTransaction(M-1000303: 2023-12-31, ADOPTION, 192.00, ref 1000303-3, some reversal, M-1000303:TRA:-192.00)");
} }
@Test @Test
@ -180,9 +174,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
"CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)", "CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)",
"CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)", "CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)",
"CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:REV:-128.00)", "CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:REV:-128.00)",
"CoopAssetsTransaction(M-1000202: 2022-10-21, REVERSAL, -128.00, ref 1000202-3, some reversal, M-1000202:DEP:+128.00)", "CoopAssetsTransaction(M-1000202: 2022-10-21, REVERSAL, -128.00, ref 1000202-3, some reversal, M-1000202:DEP:+128.00)");
"CoopAssetsTransaction(M-1000202: 2023-12-31, TRANSFER, -192.00, ref 1000202-3, some reversal, M-1000202:ADO:+192.00)",
"CoopAssetsTransaction(M-1000202: 2023-12-31, ADOPTION, 192.00, ref 1000202-3, some reversal, M-1000202:TRA:-192.00)");
} }
@Test @Test
@ -220,9 +212,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
"CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)", "CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)",
"CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)", "CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)",
"CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:REV:-128.00)", "CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:REV:-128.00)",
"CoopAssetsTransaction(M-1000101: 2022-10-21, REVERSAL, -128.00, ref 1000101-3, some reversal, M-1000101:DEP:+128.00)", "CoopAssetsTransaction(M-1000101: 2022-10-21, REVERSAL, -128.00, ref 1000101-3, some reversal, M-1000101:DEP:+128.00)");
"CoopAssetsTransaction(M-1000101: 2023-12-31, TRANSFER, -192.00, ref 1000101-3, some reversal, M-1000101:ADO:+192.00)",
"CoopAssetsTransaction(M-1000101: 2023-12-31, ADOPTION, 192.00, ref 1000101-3, some reversal, M-1000101:TRA:-192.00)");
} }
} }

View File

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

View File

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

View File

@ -17,7 +17,6 @@ import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateMembership
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDepositTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDepositTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDisbursalTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDisbursalTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsRevertTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsRevertTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsTransferTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesCancellationTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesCancellationTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesRevertTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesRevertTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesSubscriptionTransaction; import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesSubscriptionTransaction;
@ -30,15 +29,12 @@ import net.hostsharing.hsadminng.hs.office.scenarios.subscription.RemoveOperatio
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.SubscribeToMailinglist; import net.hostsharing.hsadminng.hs.office.scenarios.subscription.SubscribeToMailinglist;
import net.hostsharing.hsadminng.hs.office.scenarios.subscription.UnsubscribeFromMailinglist; import net.hostsharing.hsadminng.hs.office.scenarios.subscription.UnsubscribeFromMailinglist;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.test.IgnoreOnFailure;
import net.hostsharing.hsadminng.test.IgnoreOnFailureExtension;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
@ -55,7 +51,6 @@ import org.springframework.test.annotation.DirtiesContext;
) )
@DirtiesContext @DirtiesContext
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@ExtendWith(IgnoreOnFailureExtension.class)
class HsOfficeScenarioTests extends ScenarioTest { class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@ -82,8 +77,8 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(1011) @Order(1011)
@Produces(explicitly = "Partner: P-31011 - Michelle Matthieu", @Produces(explicitly = "Partner: P-31011 - Michelle Matthieu", implicitly = { "Person: Michelle Matthieu",
implicitly = { "Person: Michelle Matthieu", "Contact: Michelle Matthieu" }) "Contact: Michelle Matthieu" })
void shouldCreateNaturalPersonAsPartner() { void shouldCreateNaturalPersonAsPartner() {
new CreatePartner(this) new CreatePartner(this)
.given("partnerNumber", "P-31011") .given("partnerNumber", "P-31011")
@ -341,7 +336,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(4201) @Order(4201)
@Requires("Membership: M-3101000 - Test AG") @Requires("Membership: M-3101000 - Test AG")
@Produces("Coop-Shares M-3101000 - Test AG - SUBSCRIPTION Transaction") @Produces("Coop-Shares SUBSCRIPTION Transaction")
void shouldSubscribeCoopShares() { void shouldSubscribeCoopShares() {
new CreateCoopSharesSubscriptionTransaction(this) new CreateCoopSharesSubscriptionTransaction(this)
.given("memberNumber", "M-3101000") .given("memberNumber", "M-3101000")
@ -365,8 +360,8 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(4202) @Order(4202)
@Requires("Coop-Shares M-3101000 - Test AG - SUBSCRIPTION Transaction") @Requires("Coop-Shares SUBSCRIPTION Transaction")
@Produces("Coop-Shares M-3101000 - Test AG - CANCELLATION Transaction") @Produces("Coop-Shares CANCELLATION Transaction")
void shouldCancelCoopSharesSubscription() { void shouldCancelCoopSharesSubscription() {
new CreateCoopSharesCancellationTransaction(this) new CreateCoopSharesCancellationTransaction(this)
.given("memberNumber", "M-3101000") .given("memberNumber", "M-3101000")
@ -380,7 +375,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(4301) @Order(4301)
@Requires("Membership: M-3101000 - Test AG") @Requires("Membership: M-3101000 - Test AG")
@Produces("Coop-Assets M-3101000 - Test AG - DEPOSIT Transaction") @Produces("Coop-Assets DEPOSIT Transaction")
void shouldSubscribeCoopAssets() { void shouldSubscribeCoopAssets() {
new CreateCoopAssetsDepositTransaction(this) new CreateCoopAssetsDepositTransaction(this)
.given("memberNumber", "M-3101000") .given("memberNumber", "M-3101000")
@ -393,7 +388,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(4302) @Order(4302)
@Requires("Membership: M-3101000 - Test AG") @Requires("Coop-Assets DEPOSIT Transaction")
void shouldRevertCoopAssetsSubscription() { void shouldRevertCoopAssetsSubscription() {
new CreateCoopAssetsRevertTransaction(this) new CreateCoopAssetsRevertTransaction(this)
.given("memberNumber", "M-3101000") .given("memberNumber", "M-3101000")
@ -403,9 +398,9 @@ class HsOfficeScenarioTests extends ScenarioTest {
} }
@Test @Test
@Order(4303) @Order(4302)
@Requires("Coop-Assets M-3101000 - Test AG - DEPOSIT Transaction") @Requires("Coop-Assets DEPOSIT Transaction")
@Produces("Coop-Assets M-3101000 - Test AG - DISBURSAL Transaction") @Produces("Coop-Assets DISBURSAL Transaction")
void shouldDisburseCoopAssets() { void shouldDisburseCoopAssets() {
new CreateCoopAssetsDisbursalTransaction(this) new CreateCoopAssetsDisbursalTransaction(this)
.given("memberNumber", "M-3101000") .given("memberNumber", "M-3101000")
@ -416,33 +411,6 @@ class HsOfficeScenarioTests extends ScenarioTest {
.doRun(); .doRun();
} }
@Test
@Order(4304)
@Requires("Coop-Assets M-3101000 - Test AG - DEPOSIT Transaction")
@Produces("Coop-Assets M-3101000 - Test AG - TRANSFER Transaction")
void shouldTransferCoopAssets() {
new CreateCoopAssetsTransferTransaction(this)
.given("transferringMemberNumber", "M-3101000")
.given("adoptingMemberNumber", "M-4303000")
.given("reference", "transfer 2024-12-31")
.given("valueToDisburse", 2 * 64)
.given("comment", "transfer assets from M-3101000 to M-4303000")
.given("transactionDate", "2024-12-31")
.doRun();
}
@Test
@Order(4305)
@Requires("Coop-Assets M-3101000 - Test AG - TRANSFER Transaction")
@IgnoreOnFailure("TODO.impl: reverting transfers is not implemented yet")
void shouldRevertCoopAssetsTransfer() {
new CreateCoopAssetsRevertTransaction(this)
.given("memberNumber", "M-3101000")
.given("comment", "reverting some incorrect transfer transaction")
.given("dateOfIncorrectTransaction", "2024-02-15")
.doRun();
}
@Test @Test
@Order(4900) @Order(4900)
@Requires("Membership: M-3101000 - Test AG") @Requires("Membership: M-3101000 - Test AG")

View File

@ -36,7 +36,6 @@ import java.util.function.Supplier;
import static java.net.URLEncoder.encode; import static java.net.URLEncoder.encode;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.KEEP_COMMENTS; import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.KEEP_COMMENTS;
import static net.hostsharing.hsadminng.test.DebuggerDetection.isDebuggerAttached;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.platform.commons.util.StringUtils.isBlank; import static org.junit.platform.commons.util.StringUtils.isBlank;
@ -152,7 +151,7 @@ public abstract class UseCase<T extends UseCase<?>> {
.GET() .GET()
.uri(new URI("http://localhost:" + testSuite.port + uriPath)) .uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("current-subject", ScenarioTest.RUN_AS_USER) .header("current-subject", ScenarioTest.RUN_AS_USER)
.timeout(seconds(10)) .timeout(Duration.ofSeconds(10))
.build(); .build();
final var response = client.send(request, BodyHandlers.ofString()); final var response = client.send(request, BodyHandlers.ofString());
return new HttpResponse(HttpMethod.GET, uriPath, null, response); return new HttpResponse(HttpMethod.GET, uriPath, null, response);
@ -167,7 +166,7 @@ public abstract class UseCase<T extends UseCase<?>> {
.uri(new URI("http://localhost:" + testSuite.port + uriPath)) .uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("current-subject", ScenarioTest.RUN_AS_USER) .header("current-subject", ScenarioTest.RUN_AS_USER)
.timeout(seconds(10)) .timeout(Duration.ofSeconds(10))
.build(); .build();
final var response = client.send(request, BodyHandlers.ofString()); final var response = client.send(request, BodyHandlers.ofString());
return new HttpResponse(HttpMethod.POST, uriPath, requestBody, response); return new HttpResponse(HttpMethod.POST, uriPath, requestBody, response);
@ -182,7 +181,7 @@ public abstract class UseCase<T extends UseCase<?>> {
.uri(new URI("http://localhost:" + testSuite.port + uriPath)) .uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("current-subject", ScenarioTest.RUN_AS_USER) .header("current-subject", ScenarioTest.RUN_AS_USER)
.timeout(seconds(10)) .timeout(Duration.ofSeconds(10))
.build(); .build();
final var response = client.send(request, BodyHandlers.ofString()); final var response = client.send(request, BodyHandlers.ofString());
return new HttpResponse(HttpMethod.PATCH, uriPath, requestBody, response); return new HttpResponse(HttpMethod.PATCH, uriPath, requestBody, response);
@ -196,7 +195,7 @@ public abstract class UseCase<T extends UseCase<?>> {
.uri(new URI("http://localhost:" + testSuite.port + uriPath)) .uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("current-subject", ScenarioTest.RUN_AS_USER) .header("current-subject", ScenarioTest.RUN_AS_USER)
.timeout(seconds(10)) .timeout(Duration.ofSeconds(10))
.build(); .build();
final var response = client.send(request, BodyHandlers.ofString()); final var response = client.send(request, BodyHandlers.ofString());
return new HttpResponse(HttpMethod.DELETE, uriPath, null, response); return new HttpResponse(HttpMethod.DELETE, uriPath, null, response);
@ -238,10 +237,6 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
} }
private static Duration seconds(final int secondsIfNoDebuggerAttached) {
return isDebuggerAttached() ? Duration.ofHours(1) : Duration.ofSeconds(secondsIfNoDebuggerAttached);
}
public final class HttpResponse { public final class HttpResponse {
@Getter @Getter

View File

@ -10,7 +10,7 @@ public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransacti
requires("CoopAssets-Transaction with incorrect assetValue", alias -> requires("CoopAssets-Transaction with incorrect assetValue", alias ->
new CreateCoopAssetsDepositTransaction(testSuite) new CreateCoopAssetsDepositTransaction(testSuite)
.given("memberNumber", "%{memberNumber}") .given("memberNumber", "%{memberNumber}")
.given("reference", "sign %{dateOfIncorrectTransaction}") // same as relatedAssetTx .given("reference", "sign %{dateOfIncorrectTransaction}") // same as revertedAssetTx
.given("assetValue", 10) .given("assetValue", 10)
.given("comment", "coop-assets deposit transaction with wrong asset value") .given("comment", "coop-assets deposit transaction with wrong asset value")
.given("transactionDate", "%{dateOfIncorrectTransaction}") .given("transactionDate", "%{dateOfIncorrectTransaction}")
@ -20,7 +20,7 @@ public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransacti
@Override @Override
protected HttpResponse run() { protected HttpResponse run() {
given("transactionType", "REVERSAL"); given("transactionType", "REVERSAL");
given("assetValue", -10); given("assetValue", -100);
given("revertedAssetTx", uuid("CoopAssets-Transaction with incorrect assetValue")); given("revertedAssetTx", uuid("CoopAssets-Transaction with incorrect assetValue"));
return super.run(); return super.run();
} }

View File

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

View File

@ -1,46 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,20 +0,0 @@
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

@ -1,52 +0,0 @@
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

@ -1,87 +0,0 @@
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,13 +36,11 @@ 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 dump "select member_asset_id, bp_id, date, action, amount, comment
from member_asset from member_asset
WHERE bp_id NOT IN (511912)
order by member_asset_id" \ order by member_asset_id" \
"office/asset_transactions.csv" "office/asset_transactions.csv"
dump "select member_share_id, bp_id, date, action, quantity, comment dump "select member_share_id, bp_id, date, action, quantity, comment
from member_share from member_share
WHERE bp_id NOT IN (511912)
order by member_share_id" \ order by member_share_id" \
"office/share_transactions.csv" "office/share_transactions.csv"
@ -85,7 +83,7 @@ dump "select domain_id, domain_name, domain_since, domain_dns_master, domain_own
dump "select emailaddr_id, domain_id, localpart, subdomain, target dump "select emailaddr_id, domain_id, localpart, subdomain, target
from emailaddr from emailaddr
order by emailaddr_id" \ order by emailaddr_id" \
"emailaddr.csv" "hosting/emailaddr.csv"
dump "select emailalias_id, pac_id, name, target dump "select emailalias_id, pac_id, name, target
from emailalias from emailalias