OfficeScenarioTests CoopShares+Assets #121

Merged
hsh-michaelhoennig merged 39 commits from feature/use-case-acceptance-tests-4 into master 2024-11-15 11:54:19 +01:00
56 changed files with 836 additions and 247 deletions

View File

@ -95,3 +95,6 @@ if [ ! -f .environment ]; then
cp .tc-environment .environment cp .tc-environment .environment
fi fi
source .environment source .environment
alias scenario-reports-upload='./gradlew scenarioTests convertMarkdownToHtml && ssh hsh03-hsngdev@h50.hostsharing.net "rm -f doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office/*.html" && scp build/doc/scenarios/*.html hsh03-hsngdev@h50.hostsharing.net:doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office'
alias scenario-reports-open='open https://hsngdev.hs-example.de/scenarios/office'

View File

@ -410,7 +410,7 @@ tasks.register('convertMarkdownToHtml') {
group = 'Conversion' group = 'Conversion'
// Define the template file and input directory // Define the template file and input directory
def templateFile = file('doc/scenarios/template.html') def templateFile = file('doc/scenarios/.template.html')
// Task configuration and execution // Task configuration and execution
doFirst { doFirst {
@ -425,13 +425,13 @@ tasks.register('convertMarkdownToHtml') {
// Check if the template file exists // Check if the template file exists
if (!templateFile.exists()) { if (!templateFile.exists()) {
throw new GradleException("Template file 'doc/scenarios/template.html' not found.") throw new GradleException("Template file 'doc/scenarios/.template.html' not found.")
} }
} }
doLast { doLast {
// Gather all Markdown files in the current directory // Gather all Markdown files in the current directory
fileTree(dir: '.', include: 'doc/scenarios/*.md').each { file -> fileTree(dir: '.', include: 'build/doc/scenarios/*.md').each { file ->
// Corrected way to create the output file path // Corrected way to create the output file path
def outputFile = new File(file.parent, file.name.replaceAll(/\.md$/, '.html')) def outputFile = new File(file.parent, file.name.replaceAll(/\.md$/, '.html'))
@ -444,3 +444,4 @@ tasks.register('convertMarkdownToHtml') {
} }
} }
} }
convertMarkdownToHtml.dependsOn scenarioTests

110
doc/business-glossary-de.md Normal file
View File

@ -0,0 +1,110 @@
### hsadminNg fachliches Glossar
<!--
Currently, this business glossary is only available in German because in many cases,
the German terms are important for comprehensibility for those using this software.
-->
Dieses ist eine Sammlung von Fachbegriffen, die in diesem Projekt benutzt werden.
Ebenfalls aufgenommen sind technische Begriffe, die für Benutzer für das Verständnis der Schnittstellen nötig sind.
Falls etwas fehlt, bitte Bescheid geben.
#### Partner
In diesem System ist ein _Partner_ grundsätzlich jeglicher Geschäftspartner der _Hostsharing eG_.
Dies können grundsätzlich Kunden, siehe [Debitor](#Debitor), wie Lieferanten sein.
Derzeit sind aber nur Debitoren implementiert.
Des Weiteren gibt es für jeden _Partner_ eine fünfstellige Partnernummer mit dem Prefix 'P-' (z.B. `P-123454`)
sowie Zusatzinformationen (z.B. Registergerichtnummer oder Geburtsdatum), die zur genauen Identifikation benötigt werden.
Für einen _Partner_ kann es gleichzeitig mehrere [Debitoren](#Debitor)
und zeitlich nacheinander mehrere [Mitgliedschaften](#Mitgliedschaft) geben.
Partner sind grundsätzlich als ist [Relation](#Relation) der Vertragsperson mit der Person _Hostsharing eG_ implementiert.
### Debitor
Ein `Debitor` ist quasi ein Rechnungsempfänger für einen [Partner](#Partner).
Für einen _Partner_ kann es gleichzeitig mehrere [Debitoren](#Debitor) geben,
z.B. für spezielle Projekte des Kunden oder verbundene Organisationen.
Des Weiteren gibt es für jeden _Partner_ eine fünfstellige Partnernummer mit dem Prefix 'P-' (z.B. `P-123454`)
sowie Zusatzinformationen (z.B. Registergerichtsnummer oder Geburtsdatum), die zur genauen Identifikation benötigt werden.
Debitoren sind grundsätzlich als ist [Relation](#Relation) der Vertragsperson mit der Person des Vertragspartners implementiert.
#### Relation
Eine _Relation_ ist eine typisierte und mit Kontaktdaten versehene Beziehung einer (_Holder_)-Person zu einer _Anchor_-Person.
Eine Relation ist eine Art Geschäftsrolle, wir haben hier aber keinen Begriff mit 'Rolle' verwendet,
weil 'Role' (engl.) zu leicht mit der [RBAC-Rolle](#RBAC-Role) verwechselt werden könnte.
Die _Relation_ ist auch ein technisches Konzept und gehört nicht zur Domänensprache.
Dieses Konzept ist jedoch für das Verständnis der ([API](#API)) notwendig.
#### Ex-Partner
Ex-Partner bilden [Personen](#Person) ab, die vormals [Partner](#Partner) waren.
Diese bleiben dadurch informationshalber im System verfügbar.
Implementiert ist der _Ex-Partner_ als eine besondere Form der [Relation](#Relation)
der Person des Ex-Partner (_Holder_) zum neuen Partner (_Anchor_) dargestellt.
Dieses kann zu einer Kettenbildung führen.
#### Representative-Contact (ehemals _contractual_)
Ein _Representative_ ist eine natürliche Person, die für eine nicht-natürliche Person vertretungsberechtigt ist.
Implementiert ist der _Representative_ als eine besondere Form der [Relation](#Relation)
der Person des Repräsentanten (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
### VIP-Contact
Ein _VIP-Contact_ ist eine natürliche Person, die für einen Geschäftspartner eine wichtige Funktion übernimmt,
nicht aber deren offizieller Repräsentant ist.
Implementiert ist der _VIP-Contact_ als eine besondere Form der [Relation](#Relation)
der Person des VIP-Contact (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
### Operations-Contact
Ein _Operations-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner technischer Ansprechpartner ist
Implementiert ist der _Operations-Contact_ als eine besondere Form der [Relation](#Relation)
der Person des _Operations-Contact_ (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
### Subscriber-Contact
Ein _Subscriber-_Contact_ ist_ eine natürliche Person, die für einen Geschäftspartner eine bestimmte Mailingliste abonniert.
Implementiert ist der _Subscriber-Contact_ als eine besondere Form der [Relation](#Relation)
der Person des _Subscriber-Contact_ (_Holder_) zur repräsentierten Person (_Anchor_) dargestellt.
Zusätzlich wird diese Relation mit dem Kurznamen der abonnierten Mailingliste markiert.
#### Anchor / Relation-Anchor
siehe [Relation](#Relation)
#### Holder / Relation-Holder
siehe [Relation](#Relation)
#### API
Und API (Application-Programming-Interface) verstehen wir eine über HTTPS angesprochene programmatisch bedienbare Schnittstell
zur Funktionalität des hsAdmin-NG-Systems.

1
doc/scenarios/README.txt Normal file
View File

@ -0,0 +1 @@
find the generated ScenarioReports in build/doc/scenarios

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.config; package net.hostsharing.hsadminng.config;
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.openapitools.jackson.nullable.JsonNullableModule; import org.openapitools.jackson.nullable.JsonNullableModule;
@ -9,15 +10,20 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@Configuration @Configuration
public class JsonObjectMapperConfiguration { public class JsonObjectMapperConfiguration {
@Bean @Bean
@Primary @Primary
public Jackson2ObjectMapperBuilder customObjectMapper() { public Jackson2ObjectMapperBuilder customObjectMapper() {
// HOWTO: add JSON converters and specify other JSON mapping configurations
return new Jackson2ObjectMapperBuilder() return new Jackson2ObjectMapperBuilder()
.modules(new JsonNullableModule(), new JavaTimeModule()) .modules(new JsonNullableModule(), new JavaTimeModule())
.featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS, JsonParser.Feature.ALLOW_COMMENTS) .featuresToEnable(
JsonParser.Feature.ALLOW_COMMENTS,
DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS
)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
} }
} }

View File

@ -46,6 +46,7 @@ public class CustomErrorResponse {
this.path = path; this.path = path;
this.statusCode = status.value(); this.statusCode = status.value();
this.statusPhrase = status.getReasonPhrase(); this.statusPhrase = status.getReasonPhrase();
// HOWTO: debug serverside error response - set a breakpoint here
this.message = message.startsWith("ERROR: [") ? message : "ERROR: [" + statusCode + "] " + message; this.message = message.startsWith("ERROR: [") ? message : "ERROR: [" + statusCode + "] " + message;
} }
} }

View File

@ -36,7 +36,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> listCoopAssets( public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> getListOfCoopAssets(
final String currentSubject, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID membershipUuid, final UUID membershipUuid,
@ -55,7 +55,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> addCoopAssetsTransaction( public ResponseEntity<HsOfficeCoopAssetsTransactionResource> postNewCoopAssetTransaction(
final String currentSubject, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsOfficeCoopAssetsTransactionInsertResource requestBody) { final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
@ -77,7 +77,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getCoopAssetTransactionByUuid( public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getSingleCoopAssetTransactionByUuid(
final String currentSubject, final String assumedRoles, final UUID assetTransactionUuid) { final String currentSubject, final String assumedRoles, final UUID assetTransactionUuid) {
context.define(currentSubject, assumedRoles); context.define(currentSubject, assumedRoles);
@ -128,9 +128,9 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
} }
final BiConsumer<HsOfficeCoopAssetsTransactionInsertResource, HsOfficeCoopAssetsTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { final BiConsumer<HsOfficeCoopAssetsTransactionInsertResource, HsOfficeCoopAssetsTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
if ( resource.getReverseEntryUuid() != null ) { if ( resource.getRevertedAssetTxUuid() != null ) {
entity.setAdjustedAssetTx(coopAssetsTransactionRepo.findByUuid(resource.getReverseEntryUuid()) entity.setRevertedAssetTx(coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] reverseEntityUuid %s not found".formatted(resource.getReverseEntryUuid())))); .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] reverseEntityUuid %s not found".formatted(resource.getRevertedAssetTxUuid()))));
} }
}; };
}; };

View File

@ -50,8 +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(at -> ofNullable(at.getAdjustedAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null)) .withProp(at -> ofNullable(at.getRevertedAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null))
.withProp(at -> ofNullable(at.getAdjustmentAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null)) .withProp(at -> ofNullable(at.getReversalAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null))
.quotedValues(false); .quotedValues(false);
@Id @Id
@ -77,7 +77,7 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
* The signed value which directly affects the booking balance. * The signed value which directly affects the booking balance.
* *
* <p>This means, that a DEPOSIT is always positive, a DISBURSAL is always negative, * <p>This means, that a DEPOSIT is always positive, a DISBURSAL is always negative,
* but an ADJUSTMENT can bei either positive or negative. * but an REVERSAL can bei either positive or negative.
* See {@link HsOfficeCoopAssetsTransactionType} for</p> more information. * See {@link HsOfficeCoopAssetsTransactionType} for</p> more information.
*/ */
@Column(name = "assetvalue") @Column(name = "assetvalue")
@ -96,14 +96,14 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
private String comment; private String comment;
/** /**
* Optionally, the UUID of the corresponding transaction for an adjustment transaction. * Optionally, the UUID of the corresponding transaction for an reversal transaction.
*/ */
@OneToOne @OneToOne
@JoinColumn(name = "adjustedassettxuuid") @JoinColumn(name = "revertedassettxuuid")
private HsOfficeCoopAssetsTransactionEntity adjustedAssetTx; private HsOfficeCoopAssetsTransactionEntity revertedAssetTx;
@OneToOne(mappedBy = "adjustedAssetTx") @OneToOne(mappedBy = "revertedAssetTx")
private HsOfficeCoopAssetsTransactionEntity adjustmentAssetTx; private HsOfficeCoopAssetsTransactionEntity reversalAssetTx;
@Override @Override
public HsOfficeCoopAssetsTransactionEntity load() { public HsOfficeCoopAssetsTransactionEntity load() {

View File

@ -4,7 +4,7 @@ public enum HsOfficeCoopAssetsTransactionType {
/** /**
* correction of wrong bookings, value can be positive or negative * correction of wrong bookings, value can be positive or negative
*/ */
ADJUSTMENT, REVERSAL,
/** /**
* payment received from member after signing shares, value >0 * payment received from member after signing shares, value >0

View File

@ -38,7 +38,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> listCoopShares( public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> getListOfCoopShares(
final String currentSubject, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID membershipUuid, final UUID membershipUuid,
@ -57,7 +57,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeCoopSharesTransactionResource> addCoopSharesTransaction( public ResponseEntity<HsOfficeCoopSharesTransactionResource> postNewCoopSharesTransaction(
final String currentSubject, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsOfficeCoopSharesTransactionInsertResource requestBody) { final HsOfficeCoopSharesTransactionInsertResource requestBody) {
@ -80,7 +80,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsOfficeCoopSharesTransactionResource> getCoopShareTransactionByUuid( public ResponseEntity<HsOfficeCoopSharesTransactionResource> getSingleCoopShareTransactionByUuid(
final String currentSubject, final String assumedRoles, final UUID shareTransactionUuid) { final String currentSubject, final String assumedRoles, final UUID shareTransactionUuid) {
context.define(currentSubject, assumedRoles); context.define(currentSubject, assumedRoles);
@ -131,9 +131,9 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
} }
final BiConsumer<HsOfficeCoopSharesTransactionInsertResource, HsOfficeCoopSharesTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { final BiConsumer<HsOfficeCoopSharesTransactionInsertResource, HsOfficeCoopSharesTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
if ( resource.getAdjustedShareTxUuid() != null ) { if ( resource.getRevertedShareTxUuid() != null ) {
entity.setAdjustedShareTx(coopSharesTransactionRepo.findByUuid(resource.getAdjustedShareTxUuid()) entity.setRevertedShareTx(coopSharesTransactionRepo.findByUuid(resource.getRevertedShareTxUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] adjustedShareTxUuid %s not found".formatted(resource.getAdjustedShareTxUuid())))); .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] revertedShareTxUuid %s not found".formatted(resource.getRevertedShareTxUuid()))));
} }
}; };
} }

View File

@ -48,8 +48,8 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
.withProp(HsOfficeCoopSharesTransactionEntity::getShareCount) .withProp(HsOfficeCoopSharesTransactionEntity::getShareCount)
.withProp(HsOfficeCoopSharesTransactionEntity::getReference) .withProp(HsOfficeCoopSharesTransactionEntity::getReference)
.withProp(HsOfficeCoopSharesTransactionEntity::getComment) .withProp(HsOfficeCoopSharesTransactionEntity::getComment)
.withProp(at -> ofNullable(at.getAdjustedShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null)) .withProp(at -> ofNullable(at.getRevertedShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
.withProp(at -> ofNullable(at.getAdjustmentShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null)) .withProp(at -> ofNullable(at.getReversalShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
.quotedValues(false); .quotedValues(false);
@Id @Id
@ -71,7 +71,7 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
* The signed value which directly affects the booking balance. * The signed value which directly affects the booking balance.
* *
* <p>This means, that a SUBSCRIPTION is always positive, a CANCELLATION is always negative, * <p>This means, that a SUBSCRIPTION is always positive, a CANCELLATION is always negative,
* but an ADJUSTMENT can bei either positive or negative. * but an REVERSAL can bei either positive or negative.
* See {@link HsOfficeCoopSharesTransactionType} for</p> more information. * See {@link HsOfficeCoopSharesTransactionType} for</p> more information.
*/ */
@Column(name = "valuedate") @Column(name = "valuedate")
@ -93,14 +93,14 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
private String comment; private String comment;
/** /**
* Optionally, the UUID of the corresponding transaction for an adjustment transaction. * Optionally, the UUID of the corresponding transaction for a REVERSAL transaction.
*/ */
@OneToOne @OneToOne
@JoinColumn(name = "adjustedsharetxuuid") @JoinColumn(name = "revertedsharetxuuid")
private HsOfficeCoopSharesTransactionEntity adjustedShareTx; private HsOfficeCoopSharesTransactionEntity revertedShareTx;
@OneToOne(mappedBy = "adjustedShareTx") @OneToOne(mappedBy = "revertedShareTx")
private HsOfficeCoopSharesTransactionEntity adjustmentShareTx; private HsOfficeCoopSharesTransactionEntity reversalShareTx;
@Override @Override
public HsOfficeCoopSharesTransactionEntity load() { public HsOfficeCoopSharesTransactionEntity load() {

View File

@ -2,9 +2,9 @@ package net.hostsharing.hsadminng.hs.office.coopshares;
public enum HsOfficeCoopSharesTransactionType { public enum HsOfficeCoopSharesTransactionType {
/** /**
* correction of wrong bookings, with either positive or negative value * reversal of wrong bookings, with either positive or negative value identical to reversed transaction
*/ */
ADJUSTMENT, REVERSAL,
/** /**
* shares signed, e.g. with the declaration of accession, value >0 * shares signed, e.g. with the declaration of accession, value >0

View File

@ -16,6 +16,8 @@ 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;
@RestController @RestController
public class HsOfficeMembershipController implements HsOfficeMembershipsApi { public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@ -39,7 +41,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
context.define(currentSubject, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = ( memberNumber != null) final var entities = ( memberNumber != null)
? List.of(membershipRepo.findMembershipByMemberNumber(memberNumber)) ? ofNullable(membershipRepo.findMembershipByMemberNumber(memberNumber)).stream().toList()
: membershipRepo.findMembershipsByOptionalPartnerUuid(partnerUuid); : membershipRepo.findMembershipsByOptionalPartnerUuid(partnerUuid);
final var resources = mapper.mapList(entities, HsOfficeMembershipResource.class, final var resources = mapper.mapList(entities, HsOfficeMembershipResource.class,

View File

@ -14,14 +14,16 @@ public interface HsOfficeMembershipRepository extends Repository<HsOfficeMembers
HsOfficeMembershipEntity save(final HsOfficeMembershipEntity entity); HsOfficeMembershipEntity save(final HsOfficeMembershipEntity entity);
List<HsOfficeMembershipEntity> findAll();
@Query(""" @Query("""
SELECT membership FROM HsOfficeMembershipEntity membership SELECT membership FROM HsOfficeMembershipEntity membership
WHERE ( CAST(:partnerUuid as org.hibernate.type.UUIDCharType) IS NULL WHERE ( CAST(:partnerUuid as org.hibernate.type.UUIDCharType) IS NULL
OR membership.partner.uuid = :partnerUuid ) OR membership.partner.uuid = :partnerUuid )
ORDER BY membership.partner.partnerNumber, membership.memberNumberSuffix ORDER BY membership.partner.partnerNumber, membership.memberNumberSuffix
""") """)
List<HsOfficeMembershipEntity> findMembershipsByOptionalPartnerUuid(UUID partnerUuid); List<HsOfficeMembershipEntity> findMembershipsByOptionalPartnerUuid(UUID partnerUuid);
@Query(""" @Query("""
SELECT membership FROM HsOfficeMembershipEntity membership SELECT membership FROM HsOfficeMembershipEntity membership
WHERE (:partnerNumber = membership.partner.partnerNumber) WHERE (:partnerNumber = membership.partner.partnerNumber)
@ -31,10 +33,12 @@ public interface HsOfficeMembershipRepository extends Repository<HsOfficeMembers
HsOfficeMembershipEntity findMembershipByPartnerNumberAndSuffix( HsOfficeMembershipEntity findMembershipByPartnerNumberAndSuffix(
@NotNull Integer partnerNumber, @NotNull Integer partnerNumber,
@NotNull String suffix); @NotNull String suffix);
default HsOfficeMembershipEntity findMembershipByMemberNumber(Integer memberNumber) { default HsOfficeMembershipEntity findMembershipByMemberNumber(Integer memberNumber) {
final var partnerNumber = memberNumber / 100; final var partnerNumber = memberNumber / 100;
final var suffix = memberNumber % 100; final String suffix = String.format("%02d", memberNumber % 100);
return findMembershipByPartnerNumberAndSuffix(partnerNumber, String.format("%02d", suffix)); final var result = findMembershipByPartnerNumberAndSuffix(partnerNumber, suffix);
return result;
} }
long count(); long count();

View File

@ -143,6 +143,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
private void optionallyCreateExPartnerRelation(final HsOfficePartnerEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) { private void optionallyCreateExPartnerRelation(final HsOfficePartnerEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) {
if (!saved.getPartnerRel().getUuid().equals(previousPartnerRel.getUuid())) { if (!saved.getPartnerRel().getUuid().equals(previousPartnerRel.getUuid())) {
// TODO.impl: we also need to use the new partner-person as the anchor
relationRepo.save(previousPartnerRel.toBuilder().uuid(null).type(EX_PARTNER).build()); relationRepo.save(previousPartnerRel.toBuilder().uuid(null).type(EX_PARTNER).build());
} }
} }

View File

@ -4,6 +4,7 @@ import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter; import jakarta.persistence.Converter;
import java.util.stream.Stream; import java.util.stream.Stream;
// HOWTO: convert data types for exchange between PostgreSQL and Java/Hibernate/JPA-Entities
@Converter(autoApply = true) @Converter(autoApply = true)
public class HsOfficePersonTypeConverter implements AttributeConverter<HsOfficePersonType, String> { public class HsOfficePersonTypeConverter implements AttributeConverter<HsOfficePersonType, String> {

View File

@ -14,6 +14,7 @@ public class SystemProcess {
@Getter @Getter
private String stdOut; private String stdOut;
@Getter @Getter
private String stdErr; private String stdErr;
@ -21,7 +22,6 @@ public class SystemProcess {
this.processBuilder = new ProcessBuilder(command); this.processBuilder = new ProcessBuilder(command);
} }
public String getCommand() { public String getCommand() {
return processBuilder.command().toString(); return processBuilder.command().toString();
} }

View File

@ -6,7 +6,7 @@ components:
HsOfficeCoopAssetsTransactionType: HsOfficeCoopAssetsTransactionType:
type: string type: string
enum: enum:
- ADJUSTMENT - REVERSAL
- DEPOSIT - DEPOSIT
- DISBURSAL - DISBURSAL
- TRANSFER - TRANSFER
@ -32,15 +32,15 @@ components:
type: string type: string
comment: comment:
type: string type: string
adjustedAssetTx: revertedAssetTx:
$ref: '#/components/schemas/HsOfficeReferencedCoopAssetsTransaction' $ref: '#/components/schemas/HsOfficeReferencedCoopAssetsTransaction'
adjustmentAssetTx: reversalAssetTx:
$ref: '#/components/schemas/HsOfficeReferencedCoopAssetsTransaction' $ref: '#/components/schemas/HsOfficeReferencedCoopAssetsTransaction'
HsOfficeReferencedCoopAssetsTransaction: HsOfficeReferencedCoopAssetsTransaction:
description: description:
Similar to `HsOfficeCoopAssetsTransaction` but without the self-referencing properties Similar to `HsOfficeCoopAssetsTransaction` but without the self-referencing properties
(`adjustedAssetTx` and `adjustmentAssetTx`), to avoid recursive JSON. (`revertedAssetTx` and `reversalAssetTx`), to avoid recursive JSON.
type: object type: object
properties: properties:
uuid: uuid:
@ -80,7 +80,7 @@ components:
maxLength: 48 maxLength: 48
comment: comment:
type: string type: string
reverseEntry.uuid: revertedAssetTx.uuid:
type: string type: string
format: uuid format: uuid
required: required:

View File

@ -2,7 +2,7 @@ get:
tags: tags:
- hs-office-coopAssets - hs-office-coopAssets
description: 'Fetch a single asset transaction by its uuid, if visible for the current subject.' description: 'Fetch a single asset transaction by its uuid, if visible for the current subject.'
operationId: getCoopAssetTransactionByUuid operationId: getSingleCoopAssetTransactionByUuid
hsh-michaelhoennig marked this conversation as resolved Outdated

get

get
parameters: parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject' - $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles' - $ref: 'auth.yaml#/components/parameters/assumedRoles'

View File

@ -3,7 +3,7 @@ get:
description: Returns the list of (optionally filtered) cooperative asset transactions which are visible to the current subject or any of it's assumed roles. description: Returns the list of (optionally filtered) cooperative asset transactions which are visible to the current subject or any of it's assumed roles.
tags: tags:
- hs-office-coopAssets - hs-office-coopAssets
operationId: listCoopAssets operationId: getListOfCoopAssets
parameters: parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject' - $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles' - $ref: 'auth.yaml#/components/parameters/assumedRoles'
@ -46,7 +46,7 @@ post:
summary: Adds a new cooperative asset transaction. summary: Adds a new cooperative asset transaction.
tags: tags:
- hs-office-coopAssets - hs-office-coopAssets
operationId: addCoopAssetsTransaction operationId: postNewCoopAssetTransaction
parameters: parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject' - $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles' - $ref: 'auth.yaml#/components/parameters/assumedRoles'

View File

@ -6,7 +6,7 @@ components:
HsOfficeCoopSharesTransactionType: HsOfficeCoopSharesTransactionType:
type: string type: string
enum: enum:
- ADJUSTMENT - REVERSAL
- SUBSCRIPTION - SUBSCRIPTION
- CANCELLATION - CANCELLATION
@ -27,15 +27,15 @@ components:
type: string type: string
comment: comment:
type: string type: string
adjustedShareTx: revertedShareTx:
$ref: '#/components/schemas/HsOfficeReferencedCoopSharesTransaction' $ref: '#/components/schemas/HsOfficeReferencedCoopSharesTransaction'
adjustmentShareTx: reversalShareTx:
$ref: '#/components/schemas/HsOfficeReferencedCoopSharesTransaction' $ref: '#/components/schemas/HsOfficeReferencedCoopSharesTransaction'
HsOfficeReferencedCoopSharesTransaction: HsOfficeReferencedCoopSharesTransaction:
description: description:
Similar to `HsOfficeCoopSharesTransaction` but without the self-referencing properties Similar to `HsOfficeCoopSharesTransaction` but without the self-referencing properties
(`adjustedShareTx` and `adjustmentShareTx`), to avoid recursive JSON. (`revertedShareTx` and `reversalShareTx`), to avoid recursive JSON.
type: object type: object
properties: properties:
uuid: uuid:
@ -73,7 +73,7 @@ components:
maxLength: 48 maxLength: 48
comment: comment:
type: string type: string
adjustedShareTx.uuid: revertedShareTx.uuid:
type: string type: string
format: uuid format: uuid
required: required:

View File

@ -2,7 +2,7 @@ get:
tags: tags:
- hs-office-coopShares - hs-office-coopShares
description: 'Fetch a single share transaction by its uuid, if visible for the current subject.' description: 'Fetch a single share transaction by its uuid, if visible for the current subject.'
operationId: getCoopShareTransactionByUuid operationId: getSingleCoopShareTransactionByUuid
hsh-michaelhoennig marked this conversation as resolved Outdated

get

get
parameters: parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject' - $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles' - $ref: 'auth.yaml#/components/parameters/assumedRoles'

View File

@ -3,7 +3,7 @@ get:
description: Returns the list of (optionally filtered) cooperative share transactions which are visible to the current subject or any of it's assumed roles. description: Returns the list of (optionally filtered) cooperative share transactions which are visible to the current subject or any of it's assumed roles.
tags: tags:
- hs-office-coopShares - hs-office-coopShares
operationId: listCoopShares operationId: getListOfCoopShares
parameters: parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject' - $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles' - $ref: 'auth.yaml#/components/parameters/assumedRoles'
@ -46,7 +46,7 @@ post:
summary: Adds a new cooperative share transaction. summary: Adds a new cooperative share transaction.
tags: tags:
- hs-office-coopShares - hs-office-coopShares
operationId: addCoopSharesTransaction operationId: postNewCoopSharesTransaction
parameters: parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject' - $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles' - $ref: 'auth.yaml#/components/parameters/assumedRoles'

View File

@ -28,6 +28,9 @@ create table if not exists hs_office.relation
); );
--// --//
-- TODO.impl: unique constraint, to prevent using the same person multiple times as a partner, or better:
-- ( anchorUuid, holderUuid, type)
-- ============================================================================ -- ============================================================================
--changeset michael.hoennig:hs-office-relation-MAIN-TABLE-JOURNAL endDelimiter:--// --changeset michael.hoennig:hs-office-relation-MAIN-TABLE-JOURNAL endDelimiter:--//

View File

@ -4,7 +4,7 @@
--changeset michael.hoennig:hs-office-coopshares-MAIN-TABLE endDelimiter:--// --changeset michael.hoennig:hs-office-coopshares-MAIN-TABLE endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
CREATE TYPE hs_office.CoopSharesTransactionType AS ENUM ('ADJUSTMENT', 'SUBSCRIPTION', 'CANCELLATION'); CREATE TYPE hs_office.CoopSharesTransactionType AS ENUM ('REVERSAL', 'SUBSCRIPTION', 'CANCELLATION');
CREATE CAST (character varying as hs_office.CoopSharesTransactionType) WITH INOUT AS IMPLICIT; CREATE CAST (character varying as hs_office.CoopSharesTransactionType) WITH INOUT AS IMPLICIT;
@ -17,7 +17,7 @@ create table if not exists hs_office.coopsharetx
valueDate date not null, valueDate date not null,
shareCount integer not null, shareCount integer not null,
reference varchar(48) not null, reference varchar(48) not null,
adjustedShareTxUuid uuid unique REFERENCES hs_office.coopsharetx(uuid) DEFERRABLE INITIALLY DEFERRED, revertedShareTxUuid uuid unique REFERENCES hs_office.coopsharetx(uuid) DEFERRABLE INITIALLY DEFERRED,
comment varchar(512) comment varchar(512)
); );
--// --//
@ -28,8 +28,8 @@ create table if not exists hs_office.coopsharetx
alter table hs_office.coopsharetx alter table hs_office.coopsharetx
add constraint reverse_entry_missing add constraint reverse_entry_missing
check ( transactionType = 'ADJUSTMENT' and adjustedShareTxUuid is not null check ( transactionType = 'REVERSAL' and revertedShareTxUuid is not null
or transactionType <> 'ADJUSTMENT' and adjustedShareTxUuid is null); or transactionType <> 'REVERSAL' and revertedShareTxUuid is null);
--// --//
-- ============================================================================ -- ============================================================================

View File

@ -27,12 +27,12 @@ begin
raise notice 'creating test coopSharesTransaction: %', givenPartnerNumber::text || givenMemberNumberSuffix; raise notice 'creating test coopSharesTransaction: %', givenPartnerNumber::text || givenMemberNumberSuffix;
subscriptionEntryUuid := uuid_generate_v4(); subscriptionEntryUuid := uuid_generate_v4();
insert insert
into hs_office.coopsharetx(uuid, membershipuuid, transactiontype, valuedate, sharecount, reference, comment, adjustedShareTxUuid) into hs_office.coopsharetx(uuid, membershipuuid, transactiontype, valuedate, sharecount, reference, comment, revertedShareTxUuid)
values values
(uuid_generate_v4(), membership.uuid, 'SUBSCRIPTION', '2010-03-15', 4, 'ref '||givenPartnerNumber::text || givenMemberNumberSuffix||'-1', 'initial subscription', null), (uuid_generate_v4(), membership.uuid, 'SUBSCRIPTION', '2010-03-15', 4, 'ref '||givenPartnerNumber::text || givenMemberNumberSuffix||'-1', 'initial subscription', null),
(uuid_generate_v4(), membership.uuid, 'CANCELLATION', '2021-09-01', -2, 'ref '||givenPartnerNumber::text || givenMemberNumberSuffix||'-2', 'cancelling some', null), (uuid_generate_v4(), membership.uuid, 'CANCELLATION', '2021-09-01', -2, 'ref '||givenPartnerNumber::text || givenMemberNumberSuffix||'-2', 'cancelling some', null),
(subscriptionEntryUuid, membership.uuid, 'SUBSCRIPTION', '2022-10-20', 2, 'ref '||givenPartnerNumber::text || givenMemberNumberSuffix||'-3', 'some subscription', null), (subscriptionEntryUuid, membership.uuid, 'SUBSCRIPTION', '2022-10-20', 2, 'ref '||givenPartnerNumber::text || givenMemberNumberSuffix||'-3', 'some subscription', null),
(uuid_generate_v4(), membership.uuid, 'ADJUSTMENT', '2022-10-21', -2, 'ref '||givenPartnerNumber::text || givenMemberNumberSuffix||'-4', 'some adjustment', subscriptionEntryUuid); (uuid_generate_v4(), membership.uuid, 'REVERSAL', '2022-10-21', -2, 'ref '||givenPartnerNumber::text || givenMemberNumberSuffix||'-4', 'some reversal', subscriptionEntryUuid);
end; $$; end; $$;
--// --//

View File

@ -4,7 +4,7 @@
--changeset michael.hoennig:hs-office-coopassets-MAIN-TABLE endDelimiter:--// --changeset michael.hoennig:hs-office-coopassets-MAIN-TABLE endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
CREATE TYPE hs_office.CoopAssetsTransactionType AS ENUM ('ADJUSTMENT', CREATE TYPE hs_office.CoopAssetsTransactionType AS ENUM ('REVERSAL',
'DEPOSIT', 'DEPOSIT',
'DISBURSAL', 'DISBURSAL',
'TRANSFER', 'TRANSFER',
@ -22,9 +22,9 @@ create table if not exists hs_office.coopassettx
membershipUuid uuid not null references hs_office.membership(uuid), membershipUuid uuid not null references hs_office.membership(uuid),
transactionType hs_office.CoopAssetsTransactionType not null, transactionType hs_office.CoopAssetsTransactionType not null,
valueDate date not null, valueDate date not null,
assetValue money not null, assetValue numeric(12,2) not null, -- https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_money
reference varchar(48) not null, reference varchar(48) not null,
adjustedAssetTxUuid uuid unique REFERENCES hs_office.coopassettx(uuid) DEFERRABLE INITIALLY DEFERRED, revertedAssetTxUuid uuid unique REFERENCES hs_office.coopassettx(uuid) DEFERRABLE INITIALLY DEFERRED,
comment varchar(512) comment varchar(512)
); );
--// --//
@ -36,20 +36,20 @@ create table if not exists hs_office.coopassettx
alter table hs_office.coopassettx alter table hs_office.coopassettx
add constraint reverse_entry_missing add constraint reverse_entry_missing
check ( transactionType = 'ADJUSTMENT' and adjustedAssetTxUuid is not null check ( transactionType = 'REVERSAL' and revertedAssetTxUuid is not null
or transactionType <> 'ADJUSTMENT' and adjustedAssetTxUuid is null); or transactionType <> 'REVERSAL' and revertedAssetTxUuid is null);
--// --//
-- ============================================================================ -- ============================================================================
--changeset michael.hoennig:hs-office-coopassets-ASSET-VALUE-CONSTRAINT endDelimiter:--// --changeset michael.hoennig:hs-office-coopassets-ASSET-VALUE-CONSTRAINT endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
create or replace function hs_office.coopassetstx_check_positive_total(forMembershipUuid UUID, newAssetValue money) create or replace function hs_office.coopassetstx_check_positive_total(forMembershipUuid UUID, newAssetValue numeric(12, 5))
returns boolean returns boolean
language plpgsql as $$ language plpgsql as $$
declare declare
currentAssetValue money; currentAssetValue numeric(12,2);
totalAssetValue money; totalAssetValue numeric(12,2);
begin begin
select sum(cat.assetValue) select sum(cat.assetValue)
from hs_office.coopassettx cat from hs_office.coopassettx cat

View File

@ -27,12 +27,12 @@ begin
raise notice 'creating test coopAssetsTransaction: %', givenPartnerNumber || givenMemberNumberSuffix; raise notice 'creating test coopAssetsTransaction: %', givenPartnerNumber || givenMemberNumberSuffix;
lossEntryUuid := uuid_generate_v4(); lossEntryUuid := uuid_generate_v4();
insert insert
into hs_office.coopassettx(uuid, membershipuuid, transactiontype, valuedate, assetvalue, reference, comment, adjustedAssetTxUuid) 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), (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), (uuid_generate_v4(), membership.uuid, 'DISBURSAL', '2021-09-01', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-2', 'partial disbursal', null),
(lossEntryUuid, membership.uuid, 'DEPOSIT', '2022-10-20', 128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some loss', null), (lossEntryUuid, membership.uuid, 'DEPOSIT', '2022-10-20', 128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some loss', null),
(uuid_generate_v4(), membership.uuid, 'ADJUSTMENT', '2022-10-21', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some adjustment', lossEntryUuid); (uuid_generate_v4(), membership.uuid, 'REVERSAL', '2022-10-21', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some reversal', lossEntryUuid);
end; $$; end; $$;
--// --//

View File

@ -442,7 +442,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
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),
34003=CoopAssetsTransaction(M-1002000: 2016-12-31, LOSS, -20.00, 1002000, for cancellation D), 34003=CoopAssetsTransaction(M-1002000: 2016-12-31, LOSS, -20.00, 1002000, for cancellation D),
35001=CoopAssetsTransaction(M-1909000: 2024-01-15, DEPOSIT, 128.00, 1909000, for subscription E), 35001=CoopAssetsTransaction(M-1909000: 2024-01-15, DEPOSIT, 128.00, 1909000, for subscription E),
35002=CoopAssetsTransaction(M-1909000: 2024-01-20, ADJUSTMENT, -128.00, 1909000, chargeback for subscription E, M-1909000:DEP:+128.00), 35002=CoopAssetsTransaction(M-1909000: 2024-01-20, REVERSAL, -128.00, 1909000, chargeback for subscription E, M-1909000:DEP:+128.00),
358=CoopAssetsTransaction(M-1000300: 2000-12-06, DEPOSIT, 5120, 1000300, for subscription A), 358=CoopAssetsTransaction(M-1000300: 2000-12-06, DEPOSIT, 5120, 1000300, for subscription A),
442=CoopAssetsTransaction(M-1015200: 2003-07-07, DEPOSIT, 64, 1015200), 442=CoopAssetsTransaction(M-1015200: 2003-07-07, DEPOSIT, 64, 1015200),
577=CoopAssetsTransaction(M-1000300: 2011-12-12, DEPOSIT, 1024, 1000300), 577=CoopAssetsTransaction(M-1000300: 2011-12-12, DEPOSIT, 1024, 1000300),
@ -795,23 +795,23 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
? HsOfficeCoopSharesTransactionType.SUBSCRIPTION ? HsOfficeCoopSharesTransactionType.SUBSCRIPTION
: "UNSUBSCRIPTION".equals(rec.getString("action")) : "UNSUBSCRIPTION".equals(rec.getString("action"))
? HsOfficeCoopSharesTransactionType.CANCELLATION ? HsOfficeCoopSharesTransactionType.CANCELLATION
: HsOfficeCoopSharesTransactionType.ADJUSTMENT : HsOfficeCoopSharesTransactionType.REVERSAL
) )
.shareCount(rec.getInteger("quantity")) .shareCount(rec.getInteger("quantity"))
.comment(rec.getString("comment")) .comment(rec.getString("comment"))
.reference(member.getMemberNumber().toString()) .reference(member.getMemberNumber().toString())
.build(); .build();
if (shareTransaction.getTransactionType() == HsOfficeCoopSharesTransactionType.ADJUSTMENT) { if (shareTransaction.getTransactionType() == HsOfficeCoopSharesTransactionType.REVERSAL) {
final var negativeValue = -shareTransaction.getShareCount(); final var negativeValue = -shareTransaction.getShareCount();
final var adjustedShareTx = coopShares.values().stream().filter(a -> final var revertedShareTx = coopShares.values().stream().filter(a ->
a.getTransactionType() != HsOfficeCoopSharesTransactionType.ADJUSTMENT && a.getTransactionType() != HsOfficeCoopSharesTransactionType.REVERSAL &&
a.getMembership() == shareTransaction.getMembership() && a.getMembership() == shareTransaction.getMembership() &&
a.getShareCount() == negativeValue) a.getShareCount() == negativeValue)
.findAny() .findAny()
.orElseThrow(() -> new IllegalStateException( .orElseThrow(() -> new IllegalStateException(
"cannot determine share reverse entry for adjustment " + shareTransaction)); "cannot determine share reverse entry for reversal " + shareTransaction));
shareTransaction.setAdjustedShareTx(adjustedShareTx); shareTransaction.setRevertedShareTx(revertedShareTx);
} }
coopShares.put(rec.getInteger("member_share_id"), shareTransaction); coopShares.put(rec.getInteger("member_share_id"), shareTransaction);
}); });
@ -837,7 +837,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
final var assetTypeMapping = new HashMap<String, HsOfficeCoopAssetsTransactionType>() { final var assetTypeMapping = new HashMap<String, HsOfficeCoopAssetsTransactionType>() {
{ {
put("ADJUSTMENT", HsOfficeCoopAssetsTransactionType.ADJUSTMENT); put("ADJUSTMENT", HsOfficeCoopAssetsTransactionType.REVERSAL);
put("HANDOVER", HsOfficeCoopAssetsTransactionType.TRANSFER); put("HANDOVER", HsOfficeCoopAssetsTransactionType.TRANSFER);
put("ADOPTION", HsOfficeCoopAssetsTransactionType.ADOPTION); put("ADOPTION", HsOfficeCoopAssetsTransactionType.ADOPTION);
put("LOSS", HsOfficeCoopAssetsTransactionType.LOSS); put("LOSS", HsOfficeCoopAssetsTransactionType.LOSS);
@ -865,16 +865,16 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
.reference(member.getMemberNumber().toString()) .reference(member.getMemberNumber().toString())
.build(); .build();
if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.ADJUSTMENT) { if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.REVERSAL) {
final var negativeValue = assetTransaction.getAssetValue().negate(); final var negativeValue = assetTransaction.getAssetValue().negate();
final var adjustedAssetTx = coopAssets.values().stream().filter(a -> final var revertedAssetTx = coopAssets.values().stream().filter(a ->
a.getTransactionType() != HsOfficeCoopAssetsTransactionType.ADJUSTMENT && a.getTransactionType() != HsOfficeCoopAssetsTransactionType.REVERSAL &&
a.getMembership() == assetTransaction.getMembership() && a.getMembership() == assetTransaction.getMembership() &&
a.getAssetValue().equals(negativeValue)) a.getAssetValue().equals(negativeValue))
.findAny() .findAny()
.orElseThrow(() -> new IllegalStateException( .orElseThrow(() -> new IllegalStateException(
"cannot determine asset reverse entry for adjustment " + assetTransaction)); "cannot determine asset reverse entry for reversal " + assetTransaction));
assetTransaction.setAdjustedAssetTx(adjustedAssetTx); assetTransaction.setRevertedAssetTx(revertedAssetTx);
} }
coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction); coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction);

View File

@ -55,7 +55,7 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
EntityManager em; EntityManager em;
@Nested @Nested
class ListCoopAssetsTransactions { class GetListOfCoopAssetsTransactions {
@Test @Test
void globalAdmin_canViewAllCoopAssetsTransactions() { void globalAdmin_canViewAllCoopAssetsTransactions() {
@ -109,21 +109,21 @@ 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",
"adjustmentAssetTx": { "reversalAssetTx": {
"transactionType": "ADJUSTMENT", "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 adjustment" "comment": "some reversal"
} }
}, },
{ {
"transactionType": "ADJUSTMENT", "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 adjustment", "comment": "some reversal",
"adjustedAssetTx": { "revertedAssetTx": {
"transactionType": "DEPOSIT", "transactionType": "DEPOSIT",
"assetValue": 128.00, "assetValue": 128.00,
"valueDate": "2022-10-20", "valueDate": "2022-10-20",
@ -166,10 +166,10 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
} }
@Nested @Nested
class AddCoopAssetsTransaction { class PostNewCoopAssetTransaction {
@Test @Test
void globalAdmin_canAddCoopAssetsTransaction() { void globalAdmin_canPostNewCoopAssetTransaction() {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101); final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101);
@ -214,7 +214,7 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
} }
@Test @Test
void globalAdmin_canAddCoopAssetsAdjustmentTransaction() { void globalAdmin_canAddCoopAssetsReversalTransaction() {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101); final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101);
@ -238,12 +238,12 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
.body(""" .body("""
{ {
"membership.uuid": "%s", "membership.uuid": "%s",
"transactionType": "ADJUSTMENT", "transactionType": "REVERSAL",
"assetValue": %s, "assetValue": %s,
"valueDate": "2022-10-30", "valueDate": "2022-10-30",
"reference": "test ref adjustment", "reference": "test ref reversal",
"comment": "some coop assets adjustment transaction", "comment": "some coop assets reversal transaction",
"reverseEntry.uuid": "%s" "revertedAssetTx.uuid": "%s"
} }
""".formatted( """.formatted(
givenMembership.getUuid(), givenMembership.getUuid(),
@ -258,12 +258,12 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
.body("uuid", isUuidValid()) .body("uuid", isUuidValid())
.body("", lenientlyEquals(""" .body("", lenientlyEquals("""
{ {
"transactionType": "ADJUSTMENT", "transactionType": "REVERSAL",
"assetValue": -256.00, "assetValue": -256.00,
"valueDate": "2022-10-30", "valueDate": "2022-10-30",
"reference": "test ref adjustment", "reference": "test ref reversal",
"comment": "some coop assets adjustment transaction", "comment": "some coop assets reversal transaction",
"adjustedAssetTx": { "revertedAssetTx": {
"transactionType": "DEPOSIT", "transactionType": "DEPOSIT",
"assetValue": 256.00, "assetValue": 256.00,
"valueDate": "2022-10-20", "valueDate": "2022-10-20",

View File

@ -77,7 +77,7 @@ class HsOfficeCoopAssetsTransactionControllerRestTest {
ASSETS_VALUE_MUST_NOT_BE_NULL( ASSETS_VALUE_MUST_NOT_BE_NULL(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "ADJUSTMENT") .with("transactionType", "REVERSAL")
.with("assetValue", 0.00), .with("assetValue", 0.00),
"[assetValue must not be 0 but is \"0.00\"]"), "[assetValue must not be 0 but is \"0.00\"]"),

View File

@ -21,14 +21,14 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest {
.build(); .build();
final HsOfficeCoopAssetsTransactionEntity givenCoopAssetAdjustmentTransaction = HsOfficeCoopAssetsTransactionEntity.builder() final HsOfficeCoopAssetsTransactionEntity givenCoopAssetReversalTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
.membership(TEST_MEMBERSHIP) .membership(TEST_MEMBERSHIP)
.reference("some-ref") .reference("some-ref")
.valueDate(LocalDate.parse("2020-01-15")) .valueDate(LocalDate.parse("2020-01-15"))
.transactionType(HsOfficeCoopAssetsTransactionType.ADJUSTMENT) .transactionType(HsOfficeCoopAssetsTransactionType.REVERSAL)
.assetValue(new BigDecimal("-128.00")) .assetValue(new BigDecimal("-128.00"))
.comment("some comment") .comment("some comment")
.adjustedAssetTx(givenCoopAssetTransaction) .revertedAssetTx(givenCoopAssetTransaction)
.build(); .build();
final HsOfficeCoopAssetsTransactionEntity givenEmptyCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder().build(); final HsOfficeCoopAssetsTransactionEntity givenEmptyCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder().build();
@ -41,12 +41,12 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest {
} }
@Test @Test
void toStringWithReverseEntryContainsReverseEntry() { void toStringWithRevertedAssetTxContainsRevertedAssetTx() {
givenCoopAssetTransaction.setAdjustedAssetTx(givenCoopAssetAdjustmentTransaction); givenCoopAssetTransaction.setRevertedAssetTx(givenCoopAssetReversalTransaction);
final var result = givenCoopAssetTransaction.toString(); final var result = givenCoopAssetTransaction.toString();
assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref, some comment, M-1000101:ADJ:-128.00)"); assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref, some comment, M-1000101:REV:-128.00)");
} }
@Test @Test

View File

@ -69,7 +69,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
final var newCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder() final var newCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
.membership(givenMembership) .membership(givenMembership)
.transactionType(HsOfficeCoopAssetsTransactionType.DEPOSIT) .transactionType(HsOfficeCoopAssetsTransactionType.DEPOSIT)
.assetValue(new BigDecimal("128.00")) .assetValue(new BigDecimal("6400.00"))
.valueDate(LocalDate.parse("2022-10-18")) .valueDate(LocalDate.parse("2022-10-18"))
.reference("temp ref A") .reference("temp ref A")
.build(); .build();
@ -98,7 +98,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
final var newCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder() final var newCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
.membership(givenMembership) .membership(givenMembership)
.transactionType(HsOfficeCoopAssetsTransactionType.DEPOSIT) .transactionType(HsOfficeCoopAssetsTransactionType.DEPOSIT)
.assetValue(new BigDecimal("128.00")) .assetValue(new BigDecimal("6400.00"))
.valueDate(LocalDate.parse("2022-10-18")) .valueDate(LocalDate.parse("2022-10-18"))
.reference("temp ref B") .reference("temp ref B")
.build(); .build();
@ -142,18 +142,18 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
result, result,
"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:ADJ:-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, ADJUSTMENT, -128.00, ref 1000101-3, some adjustment, 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-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:ADJ:-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, ADJUSTMENT, -128.00, ref 1000202-3, some adjustment, 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-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:ADJ:-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, ADJUSTMENT, -128.00, ref 1000303-3, some adjustment, M-1000303:DEP:+128.00)"); "CoopAssetsTransaction(M-1000303: 2022-10-21, REVERSAL, -128.00, ref 1000303-3, some reversal, M-1000303:DEP:+128.00)");
} }
@Test @Test
@ -173,8 +173,8 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
result, result,
"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:ADJ:-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, ADJUSTMENT, -128.00, ref 1000202-3, some adjustment, M-1000202:DEP:+128.00)"); "CoopAssetsTransaction(M-1000202: 2022-10-21, REVERSAL, -128.00, ref 1000202-3, some reversal, M-1000202:DEP:+128.00)");
} }
@Test @Test
@ -211,8 +211,8 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
result, result,
"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:ADJ:-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, ADJUSTMENT, -128.00, ref 1000101-3, some adjustment, M-1000101:DEP:+128.00)"); "CoopAssetsTransaction(M-1000101: 2022-10-21, REVERSAL, -128.00, ref 1000101-3, some reversal, M-1000101:DEP:+128.00)");
} }
} }

View File

@ -62,7 +62,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
} }
@Nested @Nested
class ListCoopSharesTransactions { class getListOfCoopSharesTransactions {
@Test @Test
void globalAdmin_canViewAllCoopSharesTransactions() { void globalAdmin_canViewAllCoopSharesTransactions() {
@ -108,21 +108,21 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
"valueDate": "2022-10-20", "valueDate": "2022-10-20",
"reference": "ref 1000202-3", "reference": "ref 1000202-3",
"comment": "some subscription", "comment": "some subscription",
"adjustmentShareTx": { "reversalShareTx": {
"transactionType": "ADJUSTMENT", "transactionType": "REVERSAL",
"shareCount": -2, "shareCount": -2,
"valueDate": "2022-10-21", "valueDate": "2022-10-21",
"reference": "ref 1000202-4", "reference": "ref 1000202-4",
"comment": "some adjustment" "comment": "some reversal"
} }
}, },
{ {
"transactionType": "ADJUSTMENT", "transactionType": "REVERSAL",
"shareCount": -2, "shareCount": -2,
"valueDate": "2022-10-21", "valueDate": "2022-10-21",
"reference": "ref 1000202-4", "reference": "ref 1000202-4",
"comment": "some adjustment", "comment": "some reversal",
"adjustedShareTx": { "revertedShareTx": {
"transactionType": "SUBSCRIPTION", "transactionType": "SUBSCRIPTION",
"shareCount": 2, "shareCount": 2,
"valueDate": "2022-10-20", "valueDate": "2022-10-20",
@ -191,7 +191,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
} }
@Test @Test
void globalAdmin_canAddCoopSharesAdjustmentTransaction() { void globalAdmin_canAddCoopSharesReversalTransaction() {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101); final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101);
@ -213,16 +213,16 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
.header("current-subject", "superuser-alex@hostsharing.net") .header("current-subject", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(""" .body("""
{ {
"membership.uuid": "%s", "membership.uuid": "%s",
"transactionType": "ADJUSTMENT", "transactionType": "REVERSAL",
"shareCount": %s, "shareCount": %s,
"valueDate": "2022-10-30", "valueDate": "2022-10-30",
"reference": "test ref adjustment", "reference": "test reversal ref",
"comment": "some coop shares adjustment transaction", "comment": "some coop shares reversal transaction",
"adjustedShareTx.uuid": "%s" "revertedShareTx.uuid": "%s"
} }
""".formatted( """.formatted(
givenMembership.getUuid(), givenMembership.getUuid(),
-givenTransaction.getShareCount(), -givenTransaction.getShareCount(),
givenTransaction.getUuid())) givenTransaction.getUuid()))
@ -235,12 +235,12 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
.body("uuid", isUuidValid()) .body("uuid", isUuidValid())
.body("", lenientlyEquals(""" .body("", lenientlyEquals("""
{ {
"transactionType": "ADJUSTMENT", "transactionType": "REVERSAL",
"shareCount": -13, "shareCount": -13,
"valueDate": "2022-10-30", "valueDate": "2022-10-30",
"reference": "test ref adjustment", "reference": "test reversal ref",
"comment": "some coop shares adjustment transaction", "comment": "some coop shares reversal transaction",
"adjustedShareTx": { "revertedShareTx": {
"transactionType": "SUBSCRIPTION", "transactionType": "SUBSCRIPTION",
"shareCount": 13, "shareCount": 13,
"valueDate": "2022-10-20", "valueDate": "2022-10-20",

View File

@ -73,7 +73,7 @@ class HsOfficeCoopSharesTransactionControllerRestTest {
SHARES_COUNT_MUST_NOT_BE_NULL( SHARES_COUNT_MUST_NOT_BE_NULL(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "ADJUSTMENT") .with("transactionType", "REVERSAL")
.with("shareCount", 0), .with("shareCount", 0),
"[shareCount must not be 0 but is \"0\"]"), "[shareCount must not be 0 but is \"0\"]"),

View File

@ -20,14 +20,14 @@ class HsOfficeCoopSharesTransactionEntityUnitTest {
.build(); .build();
final HsOfficeCoopSharesTransactionEntity givenCoopShareAdjustmentTransaction = HsOfficeCoopSharesTransactionEntity.builder() final HsOfficeCoopSharesTransactionEntity givenCoopShareReversalTransaction = HsOfficeCoopSharesTransactionEntity.builder()
.membership(TEST_MEMBERSHIP) .membership(TEST_MEMBERSHIP)
.reference("some-ref") .reference("some-ref")
.valueDate(LocalDate.parse("2020-01-15")) .valueDate(LocalDate.parse("2020-01-15"))
.transactionType(HsOfficeCoopSharesTransactionType.ADJUSTMENT) .transactionType(HsOfficeCoopSharesTransactionType.REVERSAL)
.shareCount(-4) .shareCount(-4)
.comment("some comment") .comment("some comment")
.adjustedShareTx(givenCoopSharesTransaction) .revertedShareTx(givenCoopSharesTransaction)
.build(); .build();
final HsOfficeCoopSharesTransactionEntity givenEmptyCoopSharesTransaction = HsOfficeCoopSharesTransactionEntity.builder().build(); final HsOfficeCoopSharesTransactionEntity givenEmptyCoopSharesTransaction = HsOfficeCoopSharesTransactionEntity.builder().build();
@ -40,12 +40,12 @@ class HsOfficeCoopSharesTransactionEntityUnitTest {
} }
@Test @Test
void toStringWithReverseEntryContainsReverseEntry() { void toStringWithRevertedAssetTxContainsRevertedAssetTx() {
givenCoopSharesTransaction.setAdjustedShareTx(givenCoopShareAdjustmentTransaction); givenCoopSharesTransaction.setRevertedShareTx(givenCoopShareReversalTransaction);
final var result = givenCoopSharesTransaction.toString(); final var result = givenCoopSharesTransaction.toString();
assertThat(result).isEqualTo("CoopShareTransaction(M-1000101: 2020-01-01, SUBSCRIPTION, 4, some-ref, some comment, M-1000101:ADJ:-4)"); assertThat(result).isEqualTo("CoopShareTransaction(M-1000101: 2020-01-01, SUBSCRIPTION, 4, some-ref, some comment, M-1000101:REV:-4)");
} }
@Test @Test

View File

@ -141,18 +141,18 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase
result, result,
"CoopShareTransaction(M-1000101: 2010-03-15, SUBSCRIPTION, 4, ref 1000101-1, initial subscription)", "CoopShareTransaction(M-1000101: 2010-03-15, SUBSCRIPTION, 4, ref 1000101-1, initial subscription)",
"CoopShareTransaction(M-1000101: 2021-09-01, CANCELLATION, -2, ref 1000101-2, cancelling some)", "CoopShareTransaction(M-1000101: 2021-09-01, CANCELLATION, -2, ref 1000101-2, cancelling some)",
"CoopShareTransaction(M-1000101: 2022-10-20, SUBSCRIPTION, 2, ref 1000101-3, some subscription, M-1000101:ADJ:-2)", "CoopShareTransaction(M-1000101: 2022-10-20, SUBSCRIPTION, 2, ref 1000101-3, some subscription, M-1000101:REV:-2)",
"CoopShareTransaction(M-1000101: 2022-10-21, ADJUSTMENT, -2, ref 1000101-4, some adjustment, M-1000101:SUB:+2)", "CoopShareTransaction(M-1000101: 2022-10-21, REVERSAL, -2, ref 1000101-4, some reversal, M-1000101:SUB:+2)",
"CoopShareTransaction(M-1000202: 2010-03-15, SUBSCRIPTION, 4, ref 1000202-1, initial subscription)", "CoopShareTransaction(M-1000202: 2010-03-15, SUBSCRIPTION, 4, ref 1000202-1, initial subscription)",
"CoopShareTransaction(M-1000202: 2021-09-01, CANCELLATION, -2, ref 1000202-2, cancelling some)", "CoopShareTransaction(M-1000202: 2021-09-01, CANCELLATION, -2, ref 1000202-2, cancelling some)",
"CoopShareTransaction(M-1000202: 2022-10-20, SUBSCRIPTION, 2, ref 1000202-3, some subscription, M-1000202:ADJ:-2)", "CoopShareTransaction(M-1000202: 2022-10-20, SUBSCRIPTION, 2, ref 1000202-3, some subscription, M-1000202:REV:-2)",
"CoopShareTransaction(M-1000202: 2022-10-21, ADJUSTMENT, -2, ref 1000202-4, some adjustment, M-1000202:SUB:+2)", "CoopShareTransaction(M-1000202: 2022-10-21, REVERSAL, -2, ref 1000202-4, some reversal, M-1000202:SUB:+2)",
"CoopShareTransaction(M-1000303: 2010-03-15, SUBSCRIPTION, 4, ref 1000303-1, initial subscription)", "CoopShareTransaction(M-1000303: 2010-03-15, SUBSCRIPTION, 4, ref 1000303-1, initial subscription)",
"CoopShareTransaction(M-1000303: 2021-09-01, CANCELLATION, -2, ref 1000303-2, cancelling some)", "CoopShareTransaction(M-1000303: 2021-09-01, CANCELLATION, -2, ref 1000303-2, cancelling some)",
"CoopShareTransaction(M-1000303: 2022-10-20, SUBSCRIPTION, 2, ref 1000303-3, some subscription, M-1000303:ADJ:-2)", "CoopShareTransaction(M-1000303: 2022-10-20, SUBSCRIPTION, 2, ref 1000303-3, some subscription, M-1000303:REV:-2)",
"CoopShareTransaction(M-1000303: 2022-10-21, ADJUSTMENT, -2, ref 1000303-4, some adjustment, M-1000303:SUB:+2)"); "CoopShareTransaction(M-1000303: 2022-10-21, REVERSAL, -2, ref 1000303-4, some reversal, M-1000303:SUB:+2)");
} }
@Test @Test
@ -172,8 +172,8 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase
result, result,
"CoopShareTransaction(M-1000202: 2010-03-15, SUBSCRIPTION, 4, ref 1000202-1, initial subscription)", "CoopShareTransaction(M-1000202: 2010-03-15, SUBSCRIPTION, 4, ref 1000202-1, initial subscription)",
"CoopShareTransaction(M-1000202: 2021-09-01, CANCELLATION, -2, ref 1000202-2, cancelling some)", "CoopShareTransaction(M-1000202: 2021-09-01, CANCELLATION, -2, ref 1000202-2, cancelling some)",
"CoopShareTransaction(M-1000202: 2022-10-20, SUBSCRIPTION, 2, ref 1000202-3, some subscription, M-1000202:ADJ:-2)", "CoopShareTransaction(M-1000202: 2022-10-20, SUBSCRIPTION, 2, ref 1000202-3, some subscription, M-1000202:REV:-2)",
"CoopShareTransaction(M-1000202: 2022-10-21, ADJUSTMENT, -2, ref 1000202-4, some adjustment, M-1000202:SUB:+2)"); "CoopShareTransaction(M-1000202: 2022-10-21, REVERSAL, -2, ref 1000202-4, some reversal, M-1000202:SUB:+2)");
} }
@Test @Test
@ -210,8 +210,8 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase
result, result,
"CoopShareTransaction(M-1000101: 2010-03-15, SUBSCRIPTION, 4, ref 1000101-1, initial subscription)", "CoopShareTransaction(M-1000101: 2010-03-15, SUBSCRIPTION, 4, ref 1000101-1, initial subscription)",
"CoopShareTransaction(M-1000101: 2021-09-01, CANCELLATION, -2, ref 1000101-2, cancelling some)", "CoopShareTransaction(M-1000101: 2021-09-01, CANCELLATION, -2, ref 1000101-2, cancelling some)",
"CoopShareTransaction(M-1000101: 2022-10-20, SUBSCRIPTION, 2, ref 1000101-3, some subscription, M-1000101:ADJ:-2)", "CoopShareTransaction(M-1000101: 2022-10-20, SUBSCRIPTION, 2, ref 1000101-3, some subscription, M-1000101:REV:-2)",
"CoopShareTransaction(M-1000101: 2022-10-21, ADJUSTMENT, -2, ref 1000101-4, some adjustment, M-1000101:SUB:+2)"); "CoopShareTransaction(M-1000101: 2022-10-21, REVERSAL, -2, ref 1000101-4, some reversal, M-1000101:SUB:+2)");
} }
} }

View File

@ -20,6 +20,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.util.UUID; import java.util.UUID;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.when; 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;
@ -44,8 +45,36 @@ public class HsOfficeMembershipControllerRestTest {
@MockBean @MockBean
EntityManagerWrapper em; EntityManagerWrapper em;
@Nested
class GetMemberships {
@Test
void findMembershipByNonExistingMemberNumberReturnsEmptyList() throws Exception {
// when
mockMvc.perform(MockMvcRequestBuilders
.get("/api/hs/office/memberships?memberNumber=12345")
.header("current-subject", "superuser-alex@hostsharing.net")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"partner.uuid": null,
"memberNumberSuffix": "01",
"validFrom": "2022-10-13",
"membershipFeeBillable": "true"
}
""")
.accept(MediaType.APPLICATION_JSON))
// then
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$", hasSize(0)));
}
}
@Nested @Nested
class AddMembership { class AddMembership {
@Test @Test
void respondBadRequest_ifPartnerUuidIsMissing() throws Exception { void respondBadRequest_ifPartnerUuidIsMissing() throws Exception {
@ -98,7 +127,9 @@ public class HsOfficeMembershipControllerRestTest {
.andExpect(status().is4xxClientError()) .andExpect(status().is4xxClientError())
.andExpect(jsonPath("statusCode", is(400))) .andExpect(jsonPath("statusCode", is(400)))
.andExpect(jsonPath("statusPhrase", is("Bad Request"))) .andExpect(jsonPath("statusPhrase", is("Bad Request")))
.andExpect(jsonPath("message", is("ERROR: [400] Unable to find Partner by partner.uuid: " + givenPartnerUuid))); .andExpect(jsonPath(
"message",
is("ERROR: [400] Unable to find Partner by partner.uuid: " + givenPartnerUuid)));
} }
@ParameterizedTest @ParameterizedTest

View File

@ -13,6 +13,12 @@ import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DontDeleteDefaultDe
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.InvalidateSepaMandateForDebitor; import net.hostsharing.hsadminng.hs.office.scenarios.debitor.InvalidateSepaMandateForDebitor;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.CancelMembership; import net.hostsharing.hsadminng.hs.office.scenarios.membership.CancelMembership;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateMembership; import net.hostsharing.hsadminng.hs.office.scenarios.membership.CreateMembership;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDepositTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsDisbursalTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets.CreateCoopAssetsRevertTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesCancellationTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesRevertTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares.CreateCoopSharesSubscriptionTransaction;
import net.hostsharing.hsadminng.hs.office.scenarios.partner.AddOperationsContactToPartner; import net.hostsharing.hsadminng.hs.office.scenarios.partner.AddOperationsContactToPartner;
import net.hostsharing.hsadminng.hs.office.scenarios.partner.CreatePartner; import net.hostsharing.hsadminng.hs.office.scenarios.partner.CreatePartner;
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DeleteDebitor; import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DeleteDebitor;
@ -49,7 +55,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(1010) @Order(1010)
@Produces(explicitly = "Partner: Test AG", implicitly = {"Person: Test AG", "Contact: Test AG - Hamburg"}) @Produces(explicitly = "Partner: P-31010 - Test AG", implicitly = {"Person: Test AG", "Contact: Test AG - Hamburg"})
void shouldCreateLegalPersonAsPartner() { void shouldCreateLegalPersonAsPartner() {
new CreatePartner(this) new CreatePartner(this)
.given("partnerNumber", 31010) .given("partnerNumber", 31010)
@ -71,7 +77,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(1011) @Order(1011)
@Produces(explicitly = "Partner: Michelle Matthieu", implicitly = {"Person: Michelle Matthieu", "Contact: Michelle Matthieu"}) @Produces(explicitly = "Partner: P-31011 - Michelle Matthieu", implicitly = {"Person: Michelle Matthieu", "Contact: Michelle Matthieu"})
void shouldCreateNaturalPersonAsPartner() { void shouldCreateNaturalPersonAsPartner() {
new CreatePartner(this) new CreatePartner(this)
.given("partnerNumber", 31011) .given("partnerNumber", 31011)
@ -148,7 +154,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(1100) @Order(1100)
@Requires("Partner: Michelle Matthieu") @Requires("Partner: P-31011 - Michelle Matthieu")
void shouldAmendContactData() { void shouldAmendContactData() {
new AmendContactData(this) new AmendContactData(this)
.given("partnerName", "Matthieu") .given("partnerName", "Matthieu")
@ -158,7 +164,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(1101) @Order(1101)
@Requires("Partner: Michelle Matthieu") @Requires("Partner: P-31011 - Michelle Matthieu")
void shouldAddPhoneNumberToContactData() { void shouldAddPhoneNumberToContactData() {
new AddPhoneNumberToContactData(this) new AddPhoneNumberToContactData(this)
.given("partnerName", "Matthieu") .given("partnerName", "Matthieu")
@ -169,7 +175,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(1102) @Order(1102)
@Requires("Partner: Michelle Matthieu") @Requires("Partner: P-31011 - Michelle Matthieu")
void shouldRemovePhoneNumberFromContactData() { void shouldRemovePhoneNumberFromContactData() {
new RemovePhoneNumberFromContactData(this) new RemovePhoneNumberFromContactData(this)
.given("partnerName", "Matthieu") .given("partnerName", "Matthieu")
@ -179,7 +185,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(1103) @Order(1103)
@Requires("Partner: Test AG") @Requires("Partner: P-31010 - Test AG")
void shouldReplaceContactData() { void shouldReplaceContactData() {
new ReplaceContactData(this) new ReplaceContactData(this)
.given("partnerName", "Test AG") .given("partnerName", "Test AG")
@ -201,7 +207,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(1201) @Order(1201)
@Requires("Partner: Michelle Matthieu") @Requires("Partner: P-31011 - Michelle Matthieu")
void shouldUpdatePersonData() { void shouldUpdatePersonData() {
new ShouldUpdatePersonData(this) new ShouldUpdatePersonData(this)
.given("oldFamilyName", "Matthieu") .given("oldFamilyName", "Matthieu")
@ -211,7 +217,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(2010) @Order(2010)
@Requires("Partner: Test AG") @Requires("Partner: P-31010 - Test AG")
@Produces("Debitor: Test AG - main debitor") @Produces("Debitor: Test AG - main debitor")
void shouldCreateSelfDebitorForPartner() { void shouldCreateSelfDebitorForPartner() {
new CreateSelfDebitorForPartner(this, "Debitor: Test AG - main debitor") new CreateSelfDebitorForPartner(this, "Debitor: Test AG - main debitor")
@ -261,18 +267,18 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(2020) @Order(2020)
@Requires("Debitor: Test AG - main debitor") @Requires("Debitor: D-3101000 - Test AG - main debitor")
@Disabled("see TODO.spec in DontDeleteDefaultDebitor") @Disabled("see TODO.spec in DontDeleteDefaultDebitor")
void shouldNotDeleteDefaultDebitor() { void shouldNotDeleteDefaultDebitor() {
new DontDeleteDefaultDebitor(this) new DontDeleteDefaultDebitor(this)
.given("partnerNumber", 31020) .given("partnerNumber", 31010)
.given("debitorSuffix", "00") .given("debitorSuffix", "00")
.doRun(); .doRun();
} }
@Test @Test
@Order(3100) @Order(3100)
@Requires("Debitor: Test AG - main debitor") @Requires("Debitor: D-3101000 - Test AG - main debitor")
@Produces("SEPA-Mandate: Test AG") @Produces("SEPA-Mandate: Test AG")
void shouldCreateSepaMandateForDebitor() { void shouldCreateSepaMandateForDebitor() {
new CreateSepaMandateForDebitor(this) new CreateSepaMandateForDebitor(this)
@ -313,12 +319,11 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test @Test
@Order(4000) @Order(4000)
@Requires("Partner: Test AG") @Requires("Partner: P-31010 - Test AG")
@Produces("Membership: Test AG 00") @Produces("Membership: M-3101000 - Test AG")
void shouldCreateMembershipForPartner() { void shouldCreateMembershipForPartner() {
new CreateMembership(this) new CreateMembership(this)
.given("partnerName", "Test AG") .given("partnerName", "Test AG")
.given("memberNumberSuffix", "00")
.given("validFrom", "2024-10-15") .given("validFrom", "2024-10-15")
.given("newStatus", "ACTIVE") .given("newStatus", "ACTIVE")
.given("membershipFeeBillable", "true") .given("membershipFeeBillable", "true")
@ -326,9 +331,87 @@ class HsOfficeScenarioTests extends ScenarioTest {
.keep(); .keep();
} }
@Test
@Order(4201)
@Requires("Membership: M-3101000 - Test AG")
@Produces("Coop-Shares SUBSCRIPTION Transaction")
void shouldSubscribeCoopShares() {
new CreateCoopSharesSubscriptionTransaction(this)
.given("memberNumber", "3101000")
.given("reference", "sign 2024-01-15")
.given("shareCount", 100)
.given("comment", "Signing the Membership")
.given("transactionDate", "2024-01-15")
.doRun();
}
@Test
@Order(4202)
@Requires("Membership: M-3101000 - Test AG")
void shouldRevertCoopSharesSubscription() {
new CreateCoopSharesRevertTransaction(this)
.given("memberNumber", "3101000")
.given("comment", "reverting some incorrect transaction")
.given("dateOfIncorrectTransaction", "2024-02-15")
.doRun();
}
@Test
@Order(4202)
@Requires("Coop-Shares SUBSCRIPTION Transaction")
@Produces("Coop-Shares CANCELLATION Transaction")
void shouldCancelCoopSharesSubscription() {
new CreateCoopSharesCancellationTransaction(this)
.given("memberNumber", "3101000")
.given("reference", "cancel 2024-01-15")
.given("sharesToCancel", 8)
.given("comment", "Cancelling 8 Shares")
.given("transactionDate", "2024-02-15")
.doRun();
}
@Test
@Order(4301)
@Requires("Membership: M-3101000 - Test AG")
@Produces("Coop-Assets DEPOSIT Transaction")
void shouldSubscribeCoopAssets() {
new CreateCoopAssetsDepositTransaction(this)
.given("memberNumber", "3101000")
.given("reference", "sign 2024-01-15")
.given("assetValue", 100*64)
.given("comment", "disposal for initial shares")
.given("transactionDate", "2024-01-15")
.doRun();
}
@Test
@Order(4302)
@Requires("Membership: M-3101000 - Test AG")
void shouldRevertCoopAssetsSubscription() {
new CreateCoopAssetsRevertTransaction(this)
.given("memberNumber", "3101000")
.given("comment", "reverting some incorrect transaction")
.given("dateOfIncorrectTransaction", "2024-02-15")
.doRun();
}
@Test
@Order(4302)
@Requires("Coop-Assets DEPOSIT Transaction")
@Produces("Coop-Assets DISBURSAL Transaction")
void shouldDisburseCoopAssets() {
new CreateCoopAssetsDisbursalTransaction(this)
.given("memberNumber", "3101000")
.given("reference", "cancel 2024-01-15")
.given("valueToDisburse", 8*64)
.given("comment", "disbursal according to shares cancellation")
.given("transactionDate", "2024-02-15")
.doRun();
}
@Test @Test
@Order(4900) @Order(4900)
@Requires("Membership: Test AG 00") @Requires("Membership: M-3101000 - Test AG")
void shouldCancelMembershipOfPartner() { void shouldCancelMembershipOfPartner() {
new CancelMembership(this) new CancelMembership(this)
.given("memberNumber", "3101000") .given("memberNumber", "3101000")

View File

@ -4,6 +4,9 @@ import net.hostsharing.hsadminng.hs.office.scenarios.UseCase.HttpResponse;
import java.util.function.Consumer; import java.util.function.Consumer;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
import static org.junit.jupiter.api.Assertions.fail;
public class PathAssertion { public class PathAssertion {
private final String path; private final String path;
@ -14,10 +17,35 @@ public class PathAssertion {
@SuppressWarnings({ "unchecked", "rawtypes" }) @SuppressWarnings({ "unchecked", "rawtypes" })
public Consumer<UseCase.HttpResponse> contains(final String resolvableValue) { public Consumer<UseCase.HttpResponse> contains(final String resolvableValue) {
return response -> response.path(path).contains(ScenarioTest.resolve(resolvableValue)); return response -> {
try {
response.path(path).map(this::asString).contains(ScenarioTest.resolve(resolvableValue, DROP_COMMENTS));
} catch (final AssertionError e) {
// without this, the error message is often lacking important context
fail(e.getMessage() + " in `path(\"" + path + "\").contains(\"" + resolvableValue + "\")`" );
}
};
} }
public Consumer<HttpResponse> doesNotExist() { public Consumer<HttpResponse> doesNotExist() {
return response -> response.path(path).isNull(); // here, null Optional means key not found in JSON return response -> {
try {
response.path(path).isNull(); // here, null Optional means key not found in JSON
} catch (final AssertionError e) {
// without this, the error message is often lacking important context
fail(e.getMessage() + " in `path(\"" + path + "\").doesNotExist()`" );
}
};
}
private String asString(final Object value) {
if (value instanceof Double doubleValue) {
if (doubleValue % 1 == 0) {
return String.valueOf(doubleValue.intValue()); // avoid trailing ".0"
} else {
return doubleValue.toString();
}
}
return value.toString();
} }
} }

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.scenarios;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
import net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver;
import net.hostsharing.hsadminng.lambda.Reducer; import net.hostsharing.hsadminng.lambda.Reducer;
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest; import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
@ -26,6 +27,8 @@ import java.util.stream.Collectors;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
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 org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
public abstract class ScenarioTest extends ContextBasedTest { public abstract class ScenarioTest extends ContextBasedTest {
@ -38,11 +41,11 @@ public abstract class ScenarioTest extends ContextBasedTest {
public String toString() { public String toString() {
return ObjectUtils.toString(uuid); return ObjectUtils.toString(uuid);
} }
} }
private final static Map<String, Alias<?>> aliases = new HashMap<>(); private final static Map<String, Alias<?>> aliases = new HashMap<>();
private final static Map<String, Object> properties = new HashMap<>();
private final static Map<String, Object> properties = new HashMap<>();
public final TestReport testReport = new TestReport(aliases); public final TestReport testReport = new TestReport(aliases);
@LocalServerPort @LocalServerPort
@ -139,9 +142,9 @@ public abstract class ScenarioTest extends ContextBasedTest {
} }
static UUID uuid(final String nameWithPlaceholders) { static UUID uuid(final String nameWithPlaceholders) {
final var resoledName = resolve(nameWithPlaceholders); final var resolvedName = resolve(nameWithPlaceholders, DROP_COMMENTS);
final UUID alias = ofNullable(knowVariables().get(resoledName)).filter(v -> v instanceof UUID).map(UUID.class::cast).orElse(null); final UUID alias = ofNullable(knowVariables().get(resolvedName)).filter(v -> v instanceof UUID).map(UUID.class::cast).orElse(null);
assertThat(alias).as("alias '" + resoledName + "' not found in aliases nor in properties [" + assertThat(alias).as("alias '" + resolvedName + "' not found in aliases nor in properties [" +
knowVariables().keySet().stream().map(v -> "'" + v + "'").collect(Collectors.joining(", ")) + "]" knowVariables().keySet().stream().map(v -> "'" + v + "'").collect(Collectors.joining(", ")) + "]"
).isNotNull(); ).isNotNull();
return alias; return alias;
@ -162,13 +165,13 @@ public abstract class ScenarioTest extends ContextBasedTest {
return map; return map;
} }
public static String resolve(final String text) { public static String resolve(final String text, final Resolver resolver) {
final var resolved = new TemplateResolver(text, ScenarioTest.knowVariables()).resolve(); final var resolved = new TemplateResolver(text, ScenarioTest.knowVariables()).resolve(resolver);
return resolved; return resolved;
} }
public static Object resolveTyped(final String text) { public static Object resolveTyped(final String text) {
final var resolved = resolve(text); final var resolved = resolve(text, DROP_COMMENTS);
try { try {
return UUID.fromString(resolved); return UUID.fromString(resolved);
} catch (final IllegalArgumentException e) { } catch (final IllegalArgumentException e) {

View File

@ -10,29 +10,39 @@ import java.util.Objects;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
public class TemplateResolver { public class TemplateResolver {
private final static Pattern pattern = Pattern.compile(",(\\s*})", Pattern.MULTILINE); public enum Resolver {
private static final String IF_NOT_FOUND_SYMBOL = "???"; DROP_COMMENTS, // deletes comments ('#{whatever}' -> '')
KEEP_COMMENTS // keep comments ('#{whatever}' -> 'whatever')
hsh-michaelhoennig marked this conversation as resolved Outdated

keep

keep
}
enum PlaceholderPrefix { enum PlaceholderPrefix {
RAW('%') { RAW('%') {
@Override @Override
String convert(final Object value) { String convert(final Object value, final Resolver resolver) {
return value != null ? value.toString() : ""; return value != null ? value.toString() : "";
} }
}, },
JSON_QUOTED('$'){ JSON_QUOTED('$'){
@Override @Override
String convert(final Object value) { String convert(final Object value, final Resolver resolver) {
return jsonQuoted(value); return jsonQuoted(value);
} }
}, },
URI_ENCODED('&'){ URI_ENCODED('&'){
@Override @Override
String convert(final Object value) { String convert(final Object value, final Resolver resolver) {
return value != null ? URLEncoder.encode(value.toString(), StandardCharsets.UTF_8) : ""; return value != null ? URLEncoder.encode(value.toString(), StandardCharsets.UTF_8) : "";
} }
},
COMMENT('#'){
@Override
String convert(final Object value, final Resolver resolver) {
return resolver == DROP_COMMENTS ? "" : value.toString();
}
}; };
private final char prefixChar; private final char prefixChar;
@ -42,19 +52,24 @@ public class TemplateResolver {
} }
static boolean contains(final char givenChar) { static boolean contains(final char givenChar) {
return Arrays.stream(values()).anyMatch(p -> p.prefixChar == givenChar); return Arrays.stream(values()).anyMatch(p -> p.prefixChar == givenChar);
} }
static PlaceholderPrefix ofPrefixChar(final char givenChar) { static PlaceholderPrefix ofPrefixChar(final char givenChar) {
return Arrays.stream(values()).filter(p -> p.prefixChar == givenChar).findFirst().orElseThrow(); return Arrays.stream(values()).filter(p -> p.prefixChar == givenChar).findFirst().orElseThrow();
} }
abstract String convert(final Object value); abstract String convert(final Object value, final Resolver resolver);
} }
private static final Pattern COMMA_RIGHT_BEFORE_CLOSING_BRACE = Pattern.compile(",(\\s*})", Pattern.MULTILINE);
hsh-michaelhoennig marked this conversation as resolved Outdated

nur }?

nur }?
private static final String IF_NOT_FOUND_SYMBOL = "???";
private final String template; private final String template;
private final Map<String, Object> properties; private final Map<String, Object> properties;
private final StringBuilder resolved = new StringBuilder(); private final StringBuilder resolved = new StringBuilder();
private Resolver resolver;
private int position = 0; private int position = 0;
public TemplateResolver(final String template, final Map<String, Object> properties) { public TemplateResolver(final String template, final Map<String, Object> properties) {
@ -62,7 +77,8 @@ public class TemplateResolver {
this.properties = properties; this.properties = properties;
} }
String resolve() { String resolve(final Resolver resolver) {
this.resolver = resolver;
final var resolved = copy(); final var resolved = copy();
final var withoutDroppedLines = dropLinesWithNullProperties(resolved); final var withoutDroppedLines = dropLinesWithNullProperties(resolved);
final var result = removeDanglingCommas(withoutDroppedLines); final var result = removeDanglingCommas(withoutDroppedLines);
@ -70,7 +86,7 @@ public class TemplateResolver {
} }
private static String removeDanglingCommas(final String withoutDroppedLines) { private static String removeDanglingCommas(final String withoutDroppedLines) {
return pattern.matcher(withoutDroppedLines).replaceAll("$1"); return COMMA_RIGHT_BEFORE_CLOSING_BRACE.matcher(withoutDroppedLines).replaceAll("$1");
} }
private String dropLinesWithNullProperties(final String text) { private String dropLinesWithNullProperties(final String text) {
@ -119,10 +135,10 @@ public class TemplateResolver {
placeholder.append(fetchChar()); placeholder.append(fetchChar());
} }
} }
final var name = new TemplateResolver(placeholder.toString(), properties).resolve(); final var content = new TemplateResolver(placeholder.toString(), properties).resolve(resolver);
final var value = propVal(name); final var value = intro != '#' ? propVal(content) : content;
resolved.append( resolved.append(
PlaceholderPrefix.ofPrefixChar(intro).convert(value) PlaceholderPrefix.ofPrefixChar(intro).convert(value, resolver)
); );
skipChar('}'); skipChar('}');
} }
@ -134,12 +150,12 @@ public class TemplateResolver {
} else if (nameExpression.contains(IF_NOT_FOUND_SYMBOL)) { } else if (nameExpression.contains(IF_NOT_FOUND_SYMBOL)) {
final var parts = StringUtils.split(nameExpression, IF_NOT_FOUND_SYMBOL); final var parts = StringUtils.split(nameExpression, IF_NOT_FOUND_SYMBOL);
return Arrays.stream(parts).filter(Objects::nonNull).findFirst().orElseGet(() -> { return Arrays.stream(parts).filter(Objects::nonNull).findFirst().orElseGet(() -> {
if ( parts[parts.length-1].isEmpty() ) { if ( parts[parts.length-1].isEmpty() ) {
// => whole expression ends with IF_NOT_FOUND_SYMBOL, thus last null element was optional // => whole expression ends with IF_NOT_FOUND_SYMBOL, thus last null element was optional
return null; return null;
} }
// => last alternative element in expression was null and not optional // => last alternative element in expression was null and not optional
throw new IllegalStateException("Missing required value in property-chain: " + nameExpression); throw new IllegalStateException("Missing required value in property-chain: " + nameExpression);
}); });
} else { } else {
final var val = properties.get(nameExpression); final var val = properties.get(nameExpression);

View File

@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
import java.util.Map; import java.util.Map;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
class TemplateResolverUnitTest { class TemplateResolverUnitTest {
@ -42,7 +43,7 @@ class TemplateResolverUnitTest {
Map.entry("simple placeholder", "einfach"), Map.entry("simple placeholder", "einfach"),
Map.entry("nested placeholder", "verschachtelt"), Map.entry("nested placeholder", "verschachtelt"),
Map.entry("with-special-chars", "3&3 AG") Map.entry("with-special-chars", "3&3 AG")
)).resolve(); )).resolve(DROP_COMMENTS);
assertThat(resolved).isEqualTo(""" assertThat(resolved).isEqualTo("""
with optional JSON quotes: with optional JSON quotes:

View File

@ -1,6 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios; package net.hostsharing.hsadminng.hs.office.scenarios;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.hostsharing.hsadminng.system.SystemProcess;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestInfo;
@ -9,29 +11,41 @@ import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map; import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
public class TestReport { public class TestReport {
private final Map<String, ?> aliases; public static final File BUILD_DOC_SCENARIOS = new File("build/doc/scenarios");
private final StringBuilder markdownLog = new StringBuilder(); // records everything for debugging purposes private final static File markdownLogFile = new File(BUILD_DOC_SCENARIOS, ".last-debug-log.md");
public static final SimpleDateFormat MM_DD_YYYY_HH_MM_SS = new SimpleDateFormat("MM-dd-yyyy hh:mm:ss");
private PrintWriter markdownReport; private final Map<String, ?> aliases;
private final PrintWriter markdownLog; // records everything for debugging purposes
private File markdownReportFile;
private PrintWriter markdownReport; // records only the use-case under test, without its pre-requisites
private int silent; // do not print anything to test-report if >0 private int silent; // do not print anything to test-report if >0
static {
assertThat(BUILD_DOC_SCENARIOS.isDirectory() || BUILD_DOC_SCENARIOS.mkdirs())
.as("mkdir " + BUILD_DOC_SCENARIOS).isTrue();
}
@SneakyThrows
public TestReport(final Map<String, ?> aliases) { public TestReport(final Map<String, ?> aliases) {
this.aliases = aliases; this.aliases = aliases;
this.markdownLog = new PrintWriter(new FileWriter(markdownLogFile));
} }
public void createTestLogMarkdownFile(final TestInfo testInfo) throws IOException { public void createTestLogMarkdownFile(final TestInfo testInfo) throws IOException {
final var testMethodName = testInfo.getTestMethod().map(Method::getName).orElseThrow(); final var testMethodName = testInfo.getTestMethod().map(Method::getName).orElseThrow();
final var testMethodOrder = testInfo.getTestMethod().map(m -> m.getAnnotation(Order.class).value()).orElseThrow(); final var testMethodOrder = testInfo.getTestMethod().map(m -> m.getAnnotation(Order.class).value()).orElseThrow();
assertThat(new File("doc/scenarios/").isDirectory() || new File("doc/scenarios/").mkdirs()).as("mkdir doc/scenarios/").isTrue(); markdownReportFile = new File(BUILD_DOC_SCENARIOS, testMethodOrder + "-" + testMethodName + ".md");
markdownReport = new PrintWriter(new FileWriter("doc/scenarios/" + testMethodOrder + "-" + testMethodName + ".md")); markdownReport = new PrintWriter(new FileWriter(markdownReportFile));
print("## Scenario #" + testInfo.getTestMethod().map(TestReport::orderNumber).orElseThrow() + ": " + print("## Scenario #" + determineScenarioTitle(testInfo));
testMethodName.replaceAll("([a-z])([A-Z]+)", "$1 $2"));
} }
@SneakyThrows @SneakyThrows
@ -45,7 +59,7 @@ public class TestReport {
} }
// but the debugLog should contain all output, even if silent // but the debugLog should contain all output, even if silent
markdownLog.append(outputWithCommentsForUuids); markdownLog.print(outputWithCommentsForUuids);
} }
public void printLine(final String output) { public void printLine(final String output) {
@ -56,10 +70,32 @@ public class TestReport {
printLine("\n" +output + "\n"); printLine("\n" +output + "\n");
} }
void silent(final Runnable code) {
silent++;
code.run();
silent--;
}
public void close() { public void close() {
if (markdownReport != null) { if (markdownReport != null) {
printPara("---");
printPara("generated on " + MM_DD_YYYY_HH_MM_SS.format(new Date()) + " for branch " + currentGitBranch());
markdownReport.close(); markdownReport.close();
System.out.println("SCENARIO REPORT: " + asClickableLink(markdownReportFile));
} }
markdownLog.close();
System.out.println("DEBUG LOG: " + asClickableLink(markdownLogFile));
}
private static @NotNull String determineScenarioTitle(final TestInfo testInfo) {
final var convertedTestMethodName =
testInfo.getTestMethod().map(TestReport::orderNumber).orElseThrow() + ": " +
testInfo.getTestMethod().map(Method::getName).map(t -> t.replaceAll("([a-z])([A-Z]+)", "$1 $2")).orElseThrow();
return convertedTestMethodName.replaceAll(": should ", ": ");
}
private String asClickableLink(final File file) {
return file.toURI().toString().replace("file:/", "file:///");
} }
private static Object orderNumber(final Method method) { private static Object orderNumber(final Method method) {
@ -83,10 +119,16 @@ public class TestReport {
return result.toString(); return result.toString();
} }
void silent(final Runnable code) { @SneakyThrows
silent++; private String currentGitBranch() {
code.run(); try {
silent--; final var gitRevParse = new SystemProcess("git", "rev-parse", "--abbrev-ref", "HEAD");
gitRevParse.execute();
return gitRevParse.getStdOut().split("\\R", 2)[0];
} catch (final IOException exc) {
// TODO.test: the git call does not work in Jenkins, we have to find out why
System.err.println(exc);
return "unknown";
}
} }
} }

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.scenarios;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import lombok.Getter; import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
@ -33,6 +34,8 @@ import java.util.function.Function;
import java.util.function.Supplier; 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.KEEP_COMMENTS;
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;
@ -50,6 +53,7 @@ public abstract class UseCase<T extends UseCase<?>> {
private final Map<String, Object> givenProperties = new LinkedHashMap<>(); private final Map<String, Object> givenProperties = new LinkedHashMap<>();
private String nextTitle; // just temporary to override resultAlias for sub-use-cases private String nextTitle; // just temporary to override resultAlias for sub-use-cases
private String introduction;
public UseCase(final ScenarioTest testSuite) { public UseCase(final ScenarioTest testSuite) {
this(testSuite, getResultAliasFromProducesAnnotationInCallStack()); this(testSuite, getResultAliasFromProducesAnnotationInCallStack());
@ -71,6 +75,9 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
public final HttpResponse doRun() { public final HttpResponse doRun() {
if (introduction != null) {
testReport.printPara(introduction);
}
testReport.printPara("### Given Properties"); testReport.printPara("### Given Properties");
testReport.printLine(""" testReport.printLine("""
| name | value | | name | value |
@ -81,7 +88,7 @@ public abstract class UseCase<T extends UseCase<?>> {
testReport.silent(() -> testReport.silent(() ->
requirements.forEach((alias, factory) -> { requirements.forEach((alias, factory) -> {
if (!ScenarioTest.containsAlias(alias)) { if (!ScenarioTest.containsAlias(alias)) {
factory.apply(alias).run().keep(); factory.apply(alias).run().keepAs(alias);
} }
}) })
); );
@ -95,6 +102,11 @@ public abstract class UseCase<T extends UseCase<?>> {
protected void verify(final HttpResponse response) { protected void verify(final HttpResponse response) {
} }
public UseCase<T> introduction(final String introduction) {
this.introduction = introduction;
return this;
}
public final UseCase<T> given(final String propName, final Object propValue) { public final UseCase<T> given(final String propName, final Object propValue) {
givenProperties.put(propName, propValue); givenProperties.put(propName, propValue);
ScenarioTest.putProperty(propName, propValue); ScenarioTest.putProperty(propName, propValue);
@ -106,11 +118,11 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
public final void obtain( public final void obtain(
final String alias, final String title,
final Supplier<HttpResponse> http, final Supplier<HttpResponse> http,
final Function<HttpResponse, String> extractor, final Function<HttpResponse, String> extractor,
final String... extraInfo) { final String... extraInfo) {
withTitle(ScenarioTest.resolve(alias), () -> { withTitle(title, () -> {
final var response = http.get().keep(extractor); final var response = http.get().keep(extractor);
Arrays.stream(extraInfo).forEach(testReport::printPara); Arrays.stream(extraInfo).forEach(testReport::printPara);
return response; return response;
@ -118,15 +130,15 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
public final void obtain(final String alias, final Supplier<HttpResponse> http, final String... extraInfo) { public final void obtain(final String alias, final Supplier<HttpResponse> http, final String... extraInfo) {
withTitle(ScenarioTest.resolve(alias), () -> { withTitle(alias, () -> {
final var response = http.get().keep(); final var response = http.get().keep();
Arrays.stream(extraInfo).forEach(testReport::printPara); Arrays.stream(extraInfo).forEach(testReport::printPara);
return response; return response;
}); });
} }
public HttpResponse withTitle(final String title, final Supplier<HttpResponse> code) { public HttpResponse withTitle(final String resolvableTitle, final Supplier<HttpResponse> code) {
this.nextTitle = title; this.nextTitle = resolvableTitle;
final var response = code.get(); final var response = code.get();
this.nextTitle = null; this.nextTitle = null;
return response; return response;
@ -134,7 +146,7 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows @SneakyThrows
public final HttpResponse httpGet(final String uriPathWithPlaceholders) { public final HttpResponse httpGet(final String uriPathWithPlaceholders) {
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders); final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders, DROP_COMMENTS);
final var request = HttpRequest.newBuilder() final var request = HttpRequest.newBuilder()
.GET() .GET()
.uri(new URI("http://localhost:" + testSuite.port + uriPath)) .uri(new URI("http://localhost:" + testSuite.port + uriPath))
@ -147,7 +159,7 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows @SneakyThrows
public final HttpResponse httpPost(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) { public final HttpResponse httpPost(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) {
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders); final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders, DROP_COMMENTS);
final var requestBody = bodyJsonTemplate.resolvePlaceholders(); final var requestBody = bodyJsonTemplate.resolvePlaceholders();
final var request = HttpRequest.newBuilder() final var request = HttpRequest.newBuilder()
.POST(BodyPublishers.ofString(requestBody)) .POST(BodyPublishers.ofString(requestBody))
@ -162,7 +174,7 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows @SneakyThrows
public final HttpResponse httpPatch(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) { public final HttpResponse httpPatch(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) {
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders); final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders, DROP_COMMENTS);
final var requestBody = bodyJsonTemplate.resolvePlaceholders(); final var requestBody = bodyJsonTemplate.resolvePlaceholders();
final var request = HttpRequest.newBuilder() final var request = HttpRequest.newBuilder()
.method(HttpMethod.PATCH.toString(), BodyPublishers.ofString(requestBody)) .method(HttpMethod.PATCH.toString(), BodyPublishers.ofString(requestBody))
@ -177,7 +189,7 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows @SneakyThrows
public final HttpResponse httpDelete(final String uriPathWithPlaceholders) { public final HttpResponse httpDelete(final String uriPathWithPlaceholders) {
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders); final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders, DROP_COMMENTS);
final var request = HttpRequest.newBuilder() final var request = HttpRequest.newBuilder()
.DELETE() .DELETE()
.uri(new URI("http://localhost:" + testSuite.port + uriPath)) .uri(new URI("http://localhost:" + testSuite.port + uriPath))
@ -197,7 +209,7 @@ public abstract class UseCase<T extends UseCase<?>> {
final String title, final String title,
final Supplier<UseCase.HttpResponse> http, final Supplier<UseCase.HttpResponse> http,
final Consumer<UseCase.HttpResponse>... assertions) { final Consumer<UseCase.HttpResponse>... assertions) {
withTitle(ScenarioTest.resolve(title), () -> { withTitle(title, () -> {
final var response = http.get(); final var response = http.get();
Arrays.stream(assertions).forEach(assertion -> assertion.accept(response)); Arrays.stream(assertions).forEach(assertion -> assertion.accept(response));
return response; return response;
@ -209,7 +221,7 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
public String uriEncoded(final String text) { public String uriEncoded(final String text) {
return encode(ScenarioTest.resolve(text), StandardCharsets.UTF_8); return encode(ScenarioTest.resolve(text, DROP_COMMENTS), StandardCharsets.UTF_8);
} }
public static class JsonTemplate { public static class JsonTemplate {
@ -221,7 +233,7 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
String resolvePlaceholders() { String resolvePlaceholders() {
return ScenarioTest.resolve(template); return ScenarioTest.resolve(template, DROP_COMMENTS);
} }
} }
@ -266,7 +278,7 @@ public abstract class UseCase<T extends UseCase<?>> {
} }
public HttpResponse keep(final Function<HttpResponse, String> extractor) { public HttpResponse keep(final Function<HttpResponse, String> extractor) {
final var alias = nextTitle != null ? nextTitle : resultAlias; final var alias = nextTitle != null ? ScenarioTest.resolve(nextTitle, DROP_COMMENTS) : resultAlias;
assertThat(alias).as("cannot keep result, no alias found").isNotNull(); assertThat(alias).as("cannot keep result, no alias found").isNotNull();
final var value = extractor.apply(this); final var value = extractor.apply(this);
@ -276,15 +288,20 @@ public abstract class UseCase<T extends UseCase<?>> {
return this; return this;
} }
public HttpResponse keep() { public HttpResponse keepAs(final String alias) {
final var alias = nextTitle != null ? nextTitle : resultAlias;
assertThat(alias).as("cannot keep result, no alias found").isNotNull();
ScenarioTest.putAlias( ScenarioTest.putAlias(
alias, nonNullAlias(alias),
new ScenarioTest.Alias<>(UseCase.this.getClass(), locationUuid)); new ScenarioTest.Alias<>(UseCase.this.getClass(), locationUuid));
return this; return this;
} }
public HttpResponse keep() {
final var alias = nextTitle != null ? ScenarioTest.resolve(nextTitle, DROP_COMMENTS) : resultAlias;
assertThat(alias).as("cannot keep result, no title or alias found for locationUuid: " + locationUuid).isNotNull();
return keepAs(alias);
}
@SneakyThrows @SneakyThrows
public HttpResponse expectArrayElements(final int expectedElementCount) { public HttpResponse expectArrayElements(final int expectedElementCount) {
final var rootNode = objectMapper.readTree(response.body()); final var rootNode = objectMapper.readTree(response.body());
@ -298,20 +315,20 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows @SneakyThrows
public String getFromBody(final String path) { public String getFromBody(final String path) {
return JsonPath.parse(response.body()).read(ScenarioTest.resolve(path)); return JsonPath.parse(response.body()).read(ScenarioTest.resolve(path, DROP_COMMENTS));
} }
@SneakyThrows @SneakyThrows
public Optional<String> getFromBodyAsOptional(final String path) { public <T> Optional<T> getFromBodyAsOptional(final String path) {
try { try {
return Optional.ofNullable(JsonPath.parse(response.body()).read(ScenarioTest.resolve(path))); return Optional.ofNullable(JsonPath.parse(response.body()).read(ScenarioTest.resolve(path, DROP_COMMENTS)));
} catch (final Exception e) { } catch (final PathNotFoundException e) {
return null; // means the property did not exist at all, not that it was there with value null return null; // means the property did not exist at all, not that it was there with value null
} }
} }
@SneakyThrows @SneakyThrows
public OptionalAssert<String> path(final String path) { public <T> OptionalAssert<T> path(final String path) {
return assertThat(getFromBodyAsOptional(path)); return assertThat(getFromBodyAsOptional(path));
} }
@ -320,9 +337,9 @@ public abstract class UseCase<T extends UseCase<?>> {
// the title // the title
if (nextTitle != null) { if (nextTitle != null) {
testReport.printLine("\n### " + nextTitle + "\n"); testReport.printLine("\n### " + ScenarioTest.resolve(nextTitle, KEEP_COMMENTS) + "\n");
} else if (resultAlias != null) { } else if (resultAlias != null) {
testReport.printLine("\n### " + resultAlias + "\n"); testReport.printLine("\n### Create " + resultAlias + "\n");
} else { } else {
fail("please wrap the http...-call in the UseCase using `withTitle(...)`"); fail("please wrap the http...-call in the UseCase using `withTitle(...)`");
} }
@ -342,6 +359,13 @@ public abstract class UseCase<T extends UseCase<?>> {
testReport.printLine("```"); testReport.printLine("```");
testReport.printLine(""); testReport.printLine("");
} }
private String nonNullAlias(final String alias) {
// This marker tag should not appear in the source-code, as here is nothing to fix.
// But if it appears in generated Markdown files, it should show up when that marker tag is searched.
final var onlyVisibleInGeneratedMarkdownNotInSource = new String(new char[]{'F', 'I', 'X', 'M', 'E'});
return alias == null ? "unknown alias -- " + onlyVisibleInGeneratedMarkdownNotInSource : alias;
}
} }
protected T self() { protected T self() {

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership; package net.hostsharing.hsadminng.hs.office.scenarios.membership;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest; import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON; import static io.restassured.http.ContentType.JSON;
@ -16,10 +16,18 @@ public class CreateMembership extends UseCase<CreateMembership> {
@Override @Override
protected HttpResponse run() { protected HttpResponse run() {
obtain("Partner: %{partnerName}", () ->
httpGet("/api/hs/office/partners?name=&{partnerName}")
.expecting(OK).expecting(JSON),
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
"In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
);
return httpPost("/api/hs/office/memberships", usingJsonBody(""" return httpPost("/api/hs/office/memberships", usingJsonBody("""
{ {
"partner.uuid": ${Partner: Test AG}, "partner.uuid": ${Partner: %{partnerName}},
"memberNumberSuffix": ${memberNumberSuffix}, "memberNumberSuffix": ${%{memberNumberSuffix???}???00},
"status": "ACTIVE", "status": "ACTIVE",
"validFrom": ${validFrom}, "validFrom": ${validFrom},
"membershipFeeBillable": ${membershipFeeBillable} "membershipFeeBillable": ${membershipFeeBillable}

View File

@ -0,0 +1,12 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopAssetsDepositTransaction extends CreateCoopAssetsTransaction {
public CreateCoopAssetsDepositTransaction(final ScenarioTest testSuite) {
super(testSuite);
given("transactionType", "DEPOSIT");
}
}

View File

@ -0,0 +1,17 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopAssetsDisbursalTransaction extends CreateCoopAssetsTransaction {
public CreateCoopAssetsDisbursalTransaction(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
given("transactionType", "DISBURSAL");
given("assetValue", "-%{valueToDisburse}");
return super.run();
}
}

View File

@ -0,0 +1,27 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopAssetsRevertTransaction extends CreateCoopAssetsTransaction {
public CreateCoopAssetsRevertTransaction(final ScenarioTest testSuite) {
super(testSuite);
requires("CoopAssets-Transaction with incorrect assetValue", alias ->
new CreateCoopAssetsDepositTransaction(testSuite)
.given("memberNumber", "3101000")
.given("reference", "sign %{dateOfIncorrectTransaction}") // same as revertedAssetTx
.given("assetValue", 10)
.given("comment", "coop-assets deposit transaction with wrong asset value")
.given("transactionDate", "%{dateOfIncorrectTransaction}")
);
}
@Override
protected HttpResponse run() {
given("transactionType", "REVERSAL");
given("assetValue", -100);
given("revertedAssetTx", uuid("CoopAssets-Transaction with incorrect assetValue"));
return super.run();
}
}

View File

@ -0,0 +1,53 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopassets;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.OK;
public abstract class CreateCoopAssetsTransaction extends UseCase<CreateCoopAssetsTransaction> {
public CreateCoopAssetsTransaction(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
obtain("#{Find }membershipUuid", () ->
httpGet("/api/hs/office/memberships?memberNumber=&{memberNumber}")
.expecting(OK).expecting(JSON).expectArrayElements(1),
response -> response.getFromBody("$[0].uuid")
);
return withTitle("Create the Coop-Assets-%{transactionType} Transaction", () ->
httpPost("/api/hs/office/coopassetstransactions", usingJsonBody("""
{
"membership.uuid": ${membershipUuid},
"transactionType": ${transactionType},
"reference": ${reference},
"assetValue": ${assetValue},
"comment": ${comment},
"valueDate": ${transactionDate},
"revertedAssetTx.uuid": ${revertedAssetTx???}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
);
}
@Override
protected void verify(final HttpResponse response) {
verify("Verify Coop-Assets %{transactionType}-Transaction",
() -> httpGet("/api/hs/office/coopassetstransactions/" + response.getLocationUuid())
.expecting(HttpStatus.OK).expecting(ContentType.JSON),
path("transactionType").contains("%{transactionType}"),
path("assetValue").contains("%{assetValue}"),
path("comment").contains("%{comment}"),
path("valueDate").contains("%{transactionDate}")
);
}
}

View File

@ -0,0 +1,17 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopSharesCancellationTransaction extends CreateCoopSharesTransaction {
public CreateCoopSharesCancellationTransaction(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
given("transactionType", "CANCELLATION");
given("shareCount", "-%{sharesToCancel}");
return super.run();
}
}

View File

@ -0,0 +1,27 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopSharesRevertTransaction extends CreateCoopSharesTransaction {
public CreateCoopSharesRevertTransaction(final ScenarioTest testSuite) {
super(testSuite);
requires("CoopShares-Transaction with incorrect shareCount", alias ->
new CreateCoopSharesSubscriptionTransaction(testSuite)
.given("memberNumber", "3101000")
.given("reference", "sign %{dateOfIncorrectTransaction}") // same as revertedShareTx
.given("shareCount", 100)
.given("comment", "coop-shares subscription transaction with wrong share count")
.given("transactionDate", "%{dateOfIncorrectTransaction}")
);
}
@Override
protected HttpResponse run() {
given("transactionType", "REVERSAL");
given("shareCount", -100);
given("revertedShareTx", uuid("CoopShares-Transaction with incorrect shareCount"));
return super.run();
}
}

View File

@ -0,0 +1,12 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
public class CreateCoopSharesSubscriptionTransaction extends CreateCoopSharesTransaction {
public CreateCoopSharesSubscriptionTransaction(final ScenarioTest testSuite) {
super(testSuite);
given("transactionType", "SUBSCRIPTION");
}
}

View File

@ -0,0 +1,53 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership.coopshares;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.hs.office.scenarios.UseCase;
import net.hostsharing.hsadminng.hs.office.scenarios.ScenarioTest;
import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.OK;
public abstract class CreateCoopSharesTransaction extends UseCase<CreateCoopSharesTransaction> {
public CreateCoopSharesTransaction(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
obtain("#{Find }membershipUuid", () ->
httpGet("/api/hs/office/memberships?memberNumber=&{memberNumber}")
.expecting(OK).expecting(JSON).expectArrayElements(1),
response -> response.getFromBody("$[0].uuid")
);
return withTitle("Create the Coop-Shares-%{transactionType} Transaction", () ->
httpPost("/api/hs/office/coopsharestransactions", usingJsonBody("""
{
"membership.uuid": ${membershipUuid},
"transactionType": ${transactionType},
"reference": ${reference},
"shareCount": ${shareCount},
"comment": ${comment},
"valueDate": ${transactionDate},
"revertedShareTx.uuid": ${revertedShareTx???}
}
"""))
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
);
}
@Override
protected void verify(final HttpResponse response) {
verify("Verify Coop-Shares %{transactionType}-Transaction",
() -> httpGet("/api/hs/office/coopsharestransactions/" + response.getLocationUuid())
.expecting(HttpStatus.OK).expecting(ContentType.JSON),
path("transactionType").contains("%{transactionType}"),
path("shareCount").contains("%{shareCount}"),
path("comment").contains("%{comment}"),
path("valueDate").contains("%{transactionDate}")
);
}
}

View File

@ -16,6 +16,8 @@ public class CreatePartner extends UseCase<CreatePartner> {
public CreatePartner(final ScenarioTest testSuite) { public CreatePartner(final ScenarioTest testSuite) {
super(testSuite); super(testSuite);
introduction("A partner can be a client or a vendor, currently we only use them for clients.");
} }
@Override @Override