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
fi
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'
// 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
doFirst {
@ -425,13 +425,13 @@ tasks.register('convertMarkdownToHtml') {
// Check if the template file 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 {
// 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
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;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
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.http.converter.json.Jackson2ObjectMapperBuilder;
@Configuration
public class JsonObjectMapperConfiguration {
@Bean
@Primary
public Jackson2ObjectMapperBuilder customObjectMapper() {
// HOWTO: add JSON converters and specify other JSON mapping configurations
return new Jackson2ObjectMapperBuilder()
.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);
}
}

View File

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

View File

@ -36,7 +36,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> listCoopAssets(
public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> getListOfCoopAssets(
final String currentSubject,
final String assumedRoles,
final UUID membershipUuid,
@ -55,7 +55,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
@Override
@Transactional
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> addCoopAssetsTransaction(
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> postNewCoopAssetTransaction(
final String currentSubject,
final String assumedRoles,
final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
@ -77,7 +77,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
@Override
@Transactional(readOnly = true)
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getCoopAssetTransactionByUuid(
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getSingleCoopAssetTransactionByUuid(
final String currentSubject, final String assumedRoles, final UUID assetTransactionUuid) {
context.define(currentSubject, assumedRoles);
@ -128,9 +128,9 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
}
final BiConsumer<HsOfficeCoopAssetsTransactionInsertResource, HsOfficeCoopAssetsTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
if ( resource.getReverseEntryUuid() != null ) {
entity.setAdjustedAssetTx(coopAssetsTransactionRepo.findByUuid(resource.getReverseEntryUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] reverseEntityUuid %s not found".formatted(resource.getReverseEntryUuid()))));
if ( resource.getRevertedAssetTxUuid() != null ) {
entity.setRevertedAssetTx(coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid())
.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::getReference)
.withProp(HsOfficeCoopAssetsTransactionEntity::getComment)
.withProp(at -> ofNullable(at.getAdjustedAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null))
.withProp(at -> ofNullable(at.getAdjustmentAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null))
.withProp(at -> ofNullable(at.getRevertedAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null))
.withProp(at -> ofNullable(at.getReversalAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null))
.quotedValues(false);
@Id
@ -77,7 +77,7 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
* The signed value which directly affects the booking balance.
*
* <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.
*/
@Column(name = "assetvalue")
@ -96,14 +96,14 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
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
@JoinColumn(name = "adjustedassettxuuid")
private HsOfficeCoopAssetsTransactionEntity adjustedAssetTx;
@JoinColumn(name = "revertedassettxuuid")
private HsOfficeCoopAssetsTransactionEntity revertedAssetTx;
@OneToOne(mappedBy = "adjustedAssetTx")
private HsOfficeCoopAssetsTransactionEntity adjustmentAssetTx;
@OneToOne(mappedBy = "revertedAssetTx")
private HsOfficeCoopAssetsTransactionEntity reversalAssetTx;
@Override
public HsOfficeCoopAssetsTransactionEntity load() {

View File

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

View File

@ -38,7 +38,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> listCoopShares(
public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> getListOfCoopShares(
final String currentSubject,
final String assumedRoles,
final UUID membershipUuid,
@ -57,7 +57,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Override
@Transactional
public ResponseEntity<HsOfficeCoopSharesTransactionResource> addCoopSharesTransaction(
public ResponseEntity<HsOfficeCoopSharesTransactionResource> postNewCoopSharesTransaction(
final String currentSubject,
final String assumedRoles,
final HsOfficeCoopSharesTransactionInsertResource requestBody) {
@ -80,7 +80,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Override
@Transactional(readOnly = true)
public ResponseEntity<HsOfficeCoopSharesTransactionResource> getCoopShareTransactionByUuid(
public ResponseEntity<HsOfficeCoopSharesTransactionResource> getSingleCoopShareTransactionByUuid(
final String currentSubject, final String assumedRoles, final UUID shareTransactionUuid) {
context.define(currentSubject, assumedRoles);
@ -131,9 +131,9 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
}
final BiConsumer<HsOfficeCoopSharesTransactionInsertResource, HsOfficeCoopSharesTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
if ( resource.getAdjustedShareTxUuid() != null ) {
entity.setAdjustedShareTx(coopSharesTransactionRepo.findByUuid(resource.getAdjustedShareTxUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] adjustedShareTxUuid %s not found".formatted(resource.getAdjustedShareTxUuid()))));
if ( resource.getRevertedShareTxUuid() != null ) {
entity.setRevertedShareTx(coopSharesTransactionRepo.findByUuid(resource.getRevertedShareTxUuid())
.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::getReference)
.withProp(HsOfficeCoopSharesTransactionEntity::getComment)
.withProp(at -> ofNullable(at.getAdjustedShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
.withProp(at -> ofNullable(at.getAdjustmentShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
.withProp(at -> ofNullable(at.getRevertedShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
.withProp(at -> ofNullable(at.getReversalShareTx()).map(HsOfficeCoopSharesTransactionEntity::toShortString).orElse(null))
.quotedValues(false);
@Id
@ -71,7 +71,7 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
* The signed value which directly affects the booking balance.
*
* <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.
*/
@Column(name = "valuedate")
@ -93,14 +93,14 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
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
@JoinColumn(name = "adjustedsharetxuuid")
private HsOfficeCoopSharesTransactionEntity adjustedShareTx;
@JoinColumn(name = "revertedsharetxuuid")
private HsOfficeCoopSharesTransactionEntity revertedShareTx;
@OneToOne(mappedBy = "adjustedShareTx")
private HsOfficeCoopSharesTransactionEntity adjustmentShareTx;
@OneToOne(mappedBy = "revertedShareTx")
private HsOfficeCoopSharesTransactionEntity reversalShareTx;
@Override
public HsOfficeCoopSharesTransactionEntity load() {

View File

@ -2,9 +2,9 @@ package net.hostsharing.hsadminng.hs.office.coopshares;
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

View File

@ -16,6 +16,8 @@ import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static java.util.Optional.ofNullable;
@RestController
public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@ -39,7 +41,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
context.define(currentSubject, assumedRoles);
final var entities = ( memberNumber != null)
? List.of(membershipRepo.findMembershipByMemberNumber(memberNumber))
? ofNullable(membershipRepo.findMembershipByMemberNumber(memberNumber)).stream().toList()
: membershipRepo.findMembershipsByOptionalPartnerUuid(partnerUuid);
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);
List<HsOfficeMembershipEntity> findAll();
@Query("""
SELECT membership FROM HsOfficeMembershipEntity membership
WHERE ( CAST(:partnerUuid as org.hibernate.type.UUIDCharType) IS NULL
OR membership.partner.uuid = :partnerUuid )
ORDER BY membership.partner.partnerNumber, membership.memberNumberSuffix
""")
""")
List<HsOfficeMembershipEntity> findMembershipsByOptionalPartnerUuid(UUID partnerUuid);
@Query("""
SELECT membership FROM HsOfficeMembershipEntity membership
WHERE (:partnerNumber = membership.partner.partnerNumber)
@ -31,10 +33,12 @@ public interface HsOfficeMembershipRepository extends Repository<HsOfficeMembers
HsOfficeMembershipEntity findMembershipByPartnerNumberAndSuffix(
@NotNull Integer partnerNumber,
@NotNull String suffix);
default HsOfficeMembershipEntity findMembershipByMemberNumber(Integer memberNumber) {
final var partnerNumber = memberNumber / 100;
final var suffix = memberNumber % 100;
return findMembershipByPartnerNumberAndSuffix(partnerNumber, String.format("%02d", suffix));
final String suffix = String.format("%02d", memberNumber % 100);
final var result = findMembershipByPartnerNumberAndSuffix(partnerNumber, suffix);
return result;
}
long count();

View File

@ -143,6 +143,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
private void optionallyCreateExPartnerRelation(final HsOfficePartnerEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) {
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());
}
}

View File

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

View File

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

View File

@ -6,7 +6,7 @@ components:
HsOfficeCoopAssetsTransactionType:
type: string
enum:
- ADJUSTMENT
- REVERSAL
- DEPOSIT
- DISBURSAL
- TRANSFER
@ -32,15 +32,15 @@ components:
type: string
comment:
type: string
adjustedAssetTx:
revertedAssetTx:
$ref: '#/components/schemas/HsOfficeReferencedCoopAssetsTransaction'
adjustmentAssetTx:
reversalAssetTx:
$ref: '#/components/schemas/HsOfficeReferencedCoopAssetsTransaction'
HsOfficeReferencedCoopAssetsTransaction:
description:
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
properties:
uuid:
@ -80,7 +80,7 @@ components:
maxLength: 48
comment:
type: string
reverseEntry.uuid:
revertedAssetTx.uuid:
type: string
format: uuid
required:

View File

@ -2,7 +2,7 @@ get:
tags:
- hs-office-coopAssets
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:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $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.
tags:
- hs-office-coopAssets
operationId: listCoopAssets
operationId: getListOfCoopAssets
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
@ -46,7 +46,7 @@ post:
summary: Adds a new cooperative asset transaction.
tags:
- hs-office-coopAssets
operationId: addCoopAssetsTransaction
operationId: postNewCoopAssetTransaction
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'

View File

@ -6,7 +6,7 @@ components:
HsOfficeCoopSharesTransactionType:
type: string
enum:
- ADJUSTMENT
- REVERSAL
- SUBSCRIPTION
- CANCELLATION
@ -27,15 +27,15 @@ components:
type: string
comment:
type: string
adjustedShareTx:
revertedShareTx:
$ref: '#/components/schemas/HsOfficeReferencedCoopSharesTransaction'
adjustmentShareTx:
reversalShareTx:
$ref: '#/components/schemas/HsOfficeReferencedCoopSharesTransaction'
HsOfficeReferencedCoopSharesTransaction:
description:
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
properties:
uuid:
@ -73,7 +73,7 @@ components:
maxLength: 48
comment:
type: string
adjustedShareTx.uuid:
revertedShareTx.uuid:
type: string
format: uuid
required:

View File

@ -2,7 +2,7 @@ get:
tags:
- hs-office-coopShares
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:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $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.
tags:
- hs-office-coopShares
operationId: listCoopShares
operationId: getListOfCoopShares
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
@ -46,7 +46,7 @@ post:
summary: Adds a new cooperative share transaction.
tags:
- hs-office-coopShares
operationId: addCoopSharesTransaction
operationId: postNewCoopSharesTransaction
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $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:--//

View File

@ -4,7 +4,7 @@
--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;
@ -17,7 +17,7 @@ create table if not exists hs_office.coopsharetx
valueDate date not null,
shareCount integer 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)
);
--//
@ -28,8 +28,8 @@ create table if not exists hs_office.coopsharetx
alter table hs_office.coopsharetx
add constraint reverse_entry_missing
check ( transactionType = 'ADJUSTMENT' and adjustedShareTxUuid is not null
or transactionType <> 'ADJUSTMENT' and adjustedShareTxUuid is null);
check ( transactionType = 'REVERSAL' and revertedShareTxUuid is not null
or transactionType <> 'REVERSAL' and revertedShareTxUuid is null);
--//
-- ============================================================================

View File

@ -27,12 +27,12 @@ begin
raise notice 'creating test coopSharesTransaction: %', givenPartnerNumber::text || givenMemberNumberSuffix;
subscriptionEntryUuid := uuid_generate_v4();
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
(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),
(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; $$;
--//

View File

@ -4,7 +4,7 @@
--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',
'DISBURSAL',
'TRANSFER',
@ -22,9 +22,9 @@ create table if not exists hs_office.coopassettx
membershipUuid uuid not null references hs_office.membership(uuid),
transactionType hs_office.CoopAssetsTransactionType 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,
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)
);
--//
@ -36,20 +36,20 @@ create table if not exists hs_office.coopassettx
alter table hs_office.coopassettx
add constraint reverse_entry_missing
check ( transactionType = 'ADJUSTMENT' and adjustedAssetTxUuid is not null
or transactionType <> 'ADJUSTMENT' and adjustedAssetTxUuid is null);
check ( transactionType = 'REVERSAL' and revertedAssetTxUuid is not null
or transactionType <> 'REVERSAL' and revertedAssetTxUuid is null);
--//
-- ============================================================================
--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
language plpgsql as $$
declare
currentAssetValue money;
totalAssetValue money;
currentAssetValue numeric(12,2);
totalAssetValue numeric(12,2);
begin
select sum(cat.assetValue)
from hs_office.coopassettx cat

View File

@ -27,12 +27,12 @@ begin
raise notice 'creating test coopAssetsTransaction: %', givenPartnerNumber || givenMemberNumberSuffix;
lossEntryUuid := uuid_generate_v4();
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
(uuid_generate_v4(), membership.uuid, 'DEPOSIT', '2010-03-15', 320.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-1', 'initial deposit', null),
(uuid_generate_v4(), membership.uuid, 'DISBURSAL', '2021-09-01', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-2', 'partial disbursal', null),
(lossEntryUuid, membership.uuid, 'DEPOSIT', '2022-10-20', 128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some loss', null),
(uuid_generate_v4(), membership.uuid, '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; $$;
--//

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),
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),
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),
442=CoopAssetsTransaction(M-1015200: 2003-07-07, DEPOSIT, 64, 1015200),
577=CoopAssetsTransaction(M-1000300: 2011-12-12, DEPOSIT, 1024, 1000300),
@ -795,23 +795,23 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
? HsOfficeCoopSharesTransactionType.SUBSCRIPTION
: "UNSUBSCRIPTION".equals(rec.getString("action"))
? HsOfficeCoopSharesTransactionType.CANCELLATION
: HsOfficeCoopSharesTransactionType.ADJUSTMENT
: HsOfficeCoopSharesTransactionType.REVERSAL
)
.shareCount(rec.getInteger("quantity"))
.comment(rec.getString("comment"))
.reference(member.getMemberNumber().toString())
.build();
if (shareTransaction.getTransactionType() == HsOfficeCoopSharesTransactionType.ADJUSTMENT) {
if (shareTransaction.getTransactionType() == HsOfficeCoopSharesTransactionType.REVERSAL) {
final var negativeValue = -shareTransaction.getShareCount();
final var adjustedShareTx = coopShares.values().stream().filter(a ->
a.getTransactionType() != HsOfficeCoopSharesTransactionType.ADJUSTMENT &&
final var revertedShareTx = coopShares.values().stream().filter(a ->
a.getTransactionType() != HsOfficeCoopSharesTransactionType.REVERSAL &&
a.getMembership() == shareTransaction.getMembership() &&
a.getShareCount() == negativeValue)
.findAny()
.orElseThrow(() -> new IllegalStateException(
"cannot determine share reverse entry for adjustment " + shareTransaction));
shareTransaction.setAdjustedShareTx(adjustedShareTx);
"cannot determine share reverse entry for reversal " + shareTransaction));
shareTransaction.setRevertedShareTx(revertedShareTx);
}
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>() {
{
put("ADJUSTMENT", HsOfficeCoopAssetsTransactionType.ADJUSTMENT);
put("ADJUSTMENT", HsOfficeCoopAssetsTransactionType.REVERSAL);
put("HANDOVER", HsOfficeCoopAssetsTransactionType.TRANSFER);
put("ADOPTION", HsOfficeCoopAssetsTransactionType.ADOPTION);
put("LOSS", HsOfficeCoopAssetsTransactionType.LOSS);
@ -865,16 +865,16 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
.reference(member.getMemberNumber().toString())
.build();
if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.ADJUSTMENT) {
if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.REVERSAL) {
final var negativeValue = assetTransaction.getAssetValue().negate();
final var adjustedAssetTx = coopAssets.values().stream().filter(a ->
a.getTransactionType() != HsOfficeCoopAssetsTransactionType.ADJUSTMENT &&
final var revertedAssetTx = coopAssets.values().stream().filter(a ->
a.getTransactionType() != HsOfficeCoopAssetsTransactionType.REVERSAL &&
a.getMembership() == assetTransaction.getMembership() &&
a.getAssetValue().equals(negativeValue))
.findAny()
.orElseThrow(() -> new IllegalStateException(
"cannot determine asset reverse entry for adjustment " + assetTransaction));
assetTransaction.setAdjustedAssetTx(adjustedAssetTx);
"cannot determine asset reverse entry for reversal " + assetTransaction));
assetTransaction.setRevertedAssetTx(revertedAssetTx);
}
coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction);

View File

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

View File

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

View File

@ -21,14 +21,14 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest {
.build();
final HsOfficeCoopAssetsTransactionEntity givenCoopAssetAdjustmentTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
final HsOfficeCoopAssetsTransactionEntity givenCoopAssetReversalTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
.membership(TEST_MEMBERSHIP)
.reference("some-ref")
.valueDate(LocalDate.parse("2020-01-15"))
.transactionType(HsOfficeCoopAssetsTransactionType.ADJUSTMENT)
.transactionType(HsOfficeCoopAssetsTransactionType.REVERSAL)
.assetValue(new BigDecimal("-128.00"))
.comment("some comment")
.adjustedAssetTx(givenCoopAssetTransaction)
.revertedAssetTx(givenCoopAssetTransaction)
.build();
final HsOfficeCoopAssetsTransactionEntity givenEmptyCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder().build();
@ -41,12 +41,12 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest {
}
@Test
void toStringWithReverseEntryContainsReverseEntry() {
givenCoopAssetTransaction.setAdjustedAssetTx(givenCoopAssetAdjustmentTransaction);
void toStringWithRevertedAssetTxContainsRevertedAssetTx() {
givenCoopAssetTransaction.setRevertedAssetTx(givenCoopAssetReversalTransaction);
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

View File

@ -69,7 +69,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
final var newCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
.membership(givenMembership)
.transactionType(HsOfficeCoopAssetsTransactionType.DEPOSIT)
.assetValue(new BigDecimal("128.00"))
.assetValue(new BigDecimal("6400.00"))
.valueDate(LocalDate.parse("2022-10-18"))
.reference("temp ref A")
.build();
@ -98,7 +98,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
final var newCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
.membership(givenMembership)
.transactionType(HsOfficeCoopAssetsTransactionType.DEPOSIT)
.assetValue(new BigDecimal("128.00"))
.assetValue(new BigDecimal("6400.00"))
.valueDate(LocalDate.parse("2022-10-18"))
.reference("temp ref B")
.build();
@ -142,18 +142,18 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
result,
"CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)",
"CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)",
"CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:ADJ:-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-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:REV:-128.00)",
"CoopAssetsTransaction(M-1000101: 2022-10-21, REVERSAL, -128.00, ref 1000101-3, some reversal, M-1000101:DEP:+128.00)",
"CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)",
"CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)",
"CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:ADJ:-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-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:REV:-128.00)",
"CoopAssetsTransaction(M-1000202: 2022-10-21, REVERSAL, -128.00, ref 1000202-3, some reversal, M-1000202:DEP:+128.00)",
"CoopAssetsTransaction(M-1000303: 2010-03-15, DEPOSIT, 320.00, ref 1000303-1, initial deposit)",
"CoopAssetsTransaction(M-1000303: 2021-09-01, DISBURSAL, -128.00, ref 1000303-2, partial disbursal)",
"CoopAssetsTransaction(M-1000303: 2022-10-20, DEPOSIT, 128.00, ref 1000303-3, some loss, M-1000303:ADJ:-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-20, DEPOSIT, 128.00, ref 1000303-3, some loss, M-1000303:REV:-128.00)",
"CoopAssetsTransaction(M-1000303: 2022-10-21, REVERSAL, -128.00, ref 1000303-3, some reversal, M-1000303:DEP:+128.00)");
}
@Test
@ -173,8 +173,8 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
result,
"CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)",
"CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)",
"CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:ADJ:-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-20, DEPOSIT, 128.00, ref 1000202-3, some loss, M-1000202:REV:-128.00)",
"CoopAssetsTransaction(M-1000202: 2022-10-21, REVERSAL, -128.00, ref 1000202-3, some reversal, M-1000202:DEP:+128.00)");
}
@Test
@ -211,8 +211,8 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
result,
"CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)",
"CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)",
"CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:ADJ:-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-20, DEPOSIT, 128.00, ref 1000101-3, some loss, M-1000101:REV:-128.00)",
"CoopAssetsTransaction(M-1000101: 2022-10-21, REVERSAL, -128.00, ref 1000101-3, some reversal, M-1000101:DEP:+128.00)");
}
}

View File

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

View File

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

View File

@ -20,14 +20,14 @@ class HsOfficeCoopSharesTransactionEntityUnitTest {
.build();
final HsOfficeCoopSharesTransactionEntity givenCoopShareAdjustmentTransaction = HsOfficeCoopSharesTransactionEntity.builder()
final HsOfficeCoopSharesTransactionEntity givenCoopShareReversalTransaction = HsOfficeCoopSharesTransactionEntity.builder()
.membership(TEST_MEMBERSHIP)
.reference("some-ref")
.valueDate(LocalDate.parse("2020-01-15"))
.transactionType(HsOfficeCoopSharesTransactionType.ADJUSTMENT)
.transactionType(HsOfficeCoopSharesTransactionType.REVERSAL)
.shareCount(-4)
.comment("some comment")
.adjustedShareTx(givenCoopSharesTransaction)
.revertedShareTx(givenCoopSharesTransaction)
.build();
final HsOfficeCoopSharesTransactionEntity givenEmptyCoopSharesTransaction = HsOfficeCoopSharesTransactionEntity.builder().build();
@ -40,12 +40,12 @@ class HsOfficeCoopSharesTransactionEntityUnitTest {
}
@Test
void toStringWithReverseEntryContainsReverseEntry() {
givenCoopSharesTransaction.setAdjustedShareTx(givenCoopShareAdjustmentTransaction);
void toStringWithRevertedAssetTxContainsRevertedAssetTx() {
givenCoopSharesTransaction.setRevertedShareTx(givenCoopShareReversalTransaction);
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

View File

@ -141,18 +141,18 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase
result,
"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: 2022-10-20, SUBSCRIPTION, 2, ref 1000101-3, some subscription, M-1000101:ADJ:-2)",
"CoopShareTransaction(M-1000101: 2022-10-21, ADJUSTMENT, -2, ref 1000101-4, some adjustment, M-1000101:SUB:+2)",
"CoopShareTransaction(M-1000101: 2022-10-20, SUBSCRIPTION, 2, ref 1000101-3, some subscription, M-1000101:REV:-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: 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-21, ADJUSTMENT, -2, ref 1000202-4, some adjustment, M-1000202:SUB:+2)",
"CoopShareTransaction(M-1000202: 2022-10-20, SUBSCRIPTION, 2, ref 1000202-3, some subscription, M-1000202:REV:-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: 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-21, ADJUSTMENT, -2, ref 1000303-4, some adjustment, M-1000303:SUB:+2)");
"CoopShareTransaction(M-1000303: 2022-10-20, SUBSCRIPTION, 2, ref 1000303-3, some subscription, M-1000303:REV:-2)",
"CoopShareTransaction(M-1000303: 2022-10-21, REVERSAL, -2, ref 1000303-4, some reversal, M-1000303:SUB:+2)");
}
@Test
@ -172,8 +172,8 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase
result,
"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: 2022-10-20, SUBSCRIPTION, 2, ref 1000202-3, some subscription, M-1000202:ADJ:-2)",
"CoopShareTransaction(M-1000202: 2022-10-21, ADJUSTMENT, -2, ref 1000202-4, some adjustment, M-1000202:SUB:+2)");
"CoopShareTransaction(M-1000202: 2022-10-20, SUBSCRIPTION, 2, ref 1000202-3, some subscription, M-1000202:REV:-2)",
"CoopShareTransaction(M-1000202: 2022-10-21, REVERSAL, -2, ref 1000202-4, some reversal, M-1000202:SUB:+2)");
}
@Test
@ -210,8 +210,8 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase
result,
"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: 2022-10-20, SUBSCRIPTION, 2, ref 1000101-3, some subscription, M-1000101:ADJ:-2)",
"CoopShareTransaction(M-1000101: 2022-10-21, ADJUSTMENT, -2, ref 1000101-4, some adjustment, M-1000101:SUB:+2)");
"CoopShareTransaction(M-1000101: 2022-10-20, SUBSCRIPTION, 2, ref 1000101-3, some subscription, M-1000101:REV:-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 static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@ -44,8 +45,36 @@ public class HsOfficeMembershipControllerRestTest {
@MockBean
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
class AddMembership {
@Test
void respondBadRequest_ifPartnerUuidIsMissing() throws Exception {
@ -98,7 +127,9 @@ public class HsOfficeMembershipControllerRestTest {
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("statusCode", is(400)))
.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

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.membership.CancelMembership;
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.CreatePartner;
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DeleteDebitor;
@ -49,7 +55,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@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() {
new CreatePartner(this)
.given("partnerNumber", 31010)
@ -71,7 +77,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@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() {
new CreatePartner(this)
.given("partnerNumber", 31011)
@ -148,7 +154,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(1100)
@Requires("Partner: Michelle Matthieu")
@Requires("Partner: P-31011 - Michelle Matthieu")
void shouldAmendContactData() {
new AmendContactData(this)
.given("partnerName", "Matthieu")
@ -158,7 +164,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(1101)
@Requires("Partner: Michelle Matthieu")
@Requires("Partner: P-31011 - Michelle Matthieu")
void shouldAddPhoneNumberToContactData() {
new AddPhoneNumberToContactData(this)
.given("partnerName", "Matthieu")
@ -169,7 +175,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(1102)
@Requires("Partner: Michelle Matthieu")
@Requires("Partner: P-31011 - Michelle Matthieu")
void shouldRemovePhoneNumberFromContactData() {
new RemovePhoneNumberFromContactData(this)
.given("partnerName", "Matthieu")
@ -179,7 +185,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(1103)
@Requires("Partner: Test AG")
@Requires("Partner: P-31010 - Test AG")
void shouldReplaceContactData() {
new ReplaceContactData(this)
.given("partnerName", "Test AG")
@ -201,7 +207,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(1201)
@Requires("Partner: Michelle Matthieu")
@Requires("Partner: P-31011 - Michelle Matthieu")
void shouldUpdatePersonData() {
new ShouldUpdatePersonData(this)
.given("oldFamilyName", "Matthieu")
@ -211,7 +217,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(2010)
@Requires("Partner: Test AG")
@Requires("Partner: P-31010 - Test AG")
@Produces("Debitor: Test AG - main debitor")
void shouldCreateSelfDebitorForPartner() {
new CreateSelfDebitorForPartner(this, "Debitor: Test AG - main debitor")
@ -261,18 +267,18 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(2020)
@Requires("Debitor: Test AG - main debitor")
@Requires("Debitor: D-3101000 - Test AG - main debitor")
@Disabled("see TODO.spec in DontDeleteDefaultDebitor")
void shouldNotDeleteDefaultDebitor() {
new DontDeleteDefaultDebitor(this)
.given("partnerNumber", 31020)
.given("partnerNumber", 31010)
.given("debitorSuffix", "00")
.doRun();
}
@Test
@Order(3100)
@Requires("Debitor: Test AG - main debitor")
@Requires("Debitor: D-3101000 - Test AG - main debitor")
@Produces("SEPA-Mandate: Test AG")
void shouldCreateSepaMandateForDebitor() {
new CreateSepaMandateForDebitor(this)
@ -313,12 +319,11 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(4000)
@Requires("Partner: Test AG")
@Produces("Membership: Test AG 00")
@Requires("Partner: P-31010 - Test AG")
@Produces("Membership: M-3101000 - Test AG")
void shouldCreateMembershipForPartner() {
new CreateMembership(this)
.given("partnerName", "Test AG")
.given("memberNumberSuffix", "00")
.given("validFrom", "2024-10-15")
.given("newStatus", "ACTIVE")
.given("membershipFeeBillable", "true")
@ -326,9 +331,87 @@ class HsOfficeScenarioTests extends ScenarioTest {
.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
@Order(4900)
@Requires("Membership: Test AG 00")
@Requires("Membership: M-3101000 - Test AG")
void shouldCancelMembershipOfPartner() {
new CancelMembership(this)
.given("memberNumber", "3101000")

View File

@ -4,6 +4,9 @@ import net.hostsharing.hsadminng.hs.office.scenarios.UseCase.HttpResponse;
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 {
private final String path;
@ -14,10 +17,35 @@ public class PathAssertion {
@SuppressWarnings({ "unchecked", "rawtypes" })
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() {
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 net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
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.rbac.context.ContextBasedTest;
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.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;
public abstract class ScenarioTest extends ContextBasedTest {
@ -38,11 +41,11 @@ public abstract class ScenarioTest extends ContextBasedTest {
public String toString() {
return ObjectUtils.toString(uuid);
}
}
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);
@LocalServerPort
@ -139,9 +142,9 @@ public abstract class ScenarioTest extends ContextBasedTest {
}
static UUID uuid(final String nameWithPlaceholders) {
final var resoledName = resolve(nameWithPlaceholders);
final UUID alias = ofNullable(knowVariables().get(resoledName)).filter(v -> v instanceof UUID).map(UUID.class::cast).orElse(null);
assertThat(alias).as("alias '" + resoledName + "' not found in aliases nor in properties [" +
final var resolvedName = resolve(nameWithPlaceholders, DROP_COMMENTS);
final UUID alias = ofNullable(knowVariables().get(resolvedName)).filter(v -> v instanceof UUID).map(UUID.class::cast).orElse(null);
assertThat(alias).as("alias '" + resolvedName + "' not found in aliases nor in properties [" +
knowVariables().keySet().stream().map(v -> "'" + v + "'").collect(Collectors.joining(", ")) + "]"
).isNotNull();
return alias;
@ -162,13 +165,13 @@ public abstract class ScenarioTest extends ContextBasedTest {
return map;
}
public static String resolve(final String text) {
final var resolved = new TemplateResolver(text, ScenarioTest.knowVariables()).resolve();
public static String resolve(final String text, final Resolver resolver) {
final var resolved = new TemplateResolver(text, ScenarioTest.knowVariables()).resolve(resolver);
return resolved;
}
public static Object resolveTyped(final String text) {
final var resolved = resolve(text);
final var resolved = resolve(text, DROP_COMMENTS);
try {
return UUID.fromString(resolved);
} catch (final IllegalArgumentException e) {

View File

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

keep

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

View File

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

View File

@ -1,6 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios;
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.TestInfo;
@ -9,29 +11,41 @@ import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class TestReport {
private final Map<String, ?> aliases;
private final StringBuilder markdownLog = new StringBuilder(); // records everything for debugging purposes
public static final File BUILD_DOC_SCENARIOS = new File("build/doc/scenarios");
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
static {
assertThat(BUILD_DOC_SCENARIOS.isDirectory() || BUILD_DOC_SCENARIOS.mkdirs())
.as("mkdir " + BUILD_DOC_SCENARIOS).isTrue();
}
@SneakyThrows
public TestReport(final Map<String, ?> aliases) {
this.aliases = aliases;
this.markdownLog = new PrintWriter(new FileWriter(markdownLogFile));
}
public void createTestLogMarkdownFile(final TestInfo testInfo) throws IOException {
final var testMethodName = testInfo.getTestMethod().map(Method::getName).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();
markdownReport = new PrintWriter(new FileWriter("doc/scenarios/" + testMethodOrder + "-" + testMethodName + ".md"));
print("## Scenario #" + testInfo.getTestMethod().map(TestReport::orderNumber).orElseThrow() + ": " +
testMethodName.replaceAll("([a-z])([A-Z]+)", "$1 $2"));
markdownReportFile = new File(BUILD_DOC_SCENARIOS, testMethodOrder + "-" + testMethodName + ".md");
markdownReport = new PrintWriter(new FileWriter(markdownReportFile));
print("## Scenario #" + determineScenarioTitle(testInfo));
}
@SneakyThrows
@ -45,7 +59,7 @@ public class TestReport {
}
// but the debugLog should contain all output, even if silent
markdownLog.append(outputWithCommentsForUuids);
markdownLog.print(outputWithCommentsForUuids);
}
public void printLine(final String output) {
@ -56,10 +70,32 @@ public class TestReport {
printLine("\n" +output + "\n");
}
void silent(final Runnable code) {
silent++;
code.run();
silent--;
}
public void close() {
if (markdownReport != null) {
printPara("---");
printPara("generated on " + MM_DD_YYYY_HH_MM_SS.format(new Date()) + " for branch " + currentGitBranch());
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) {
@ -83,10 +119,16 @@ public class TestReport {
return result.toString();
}
void silent(final Runnable code) {
silent++;
code.run();
silent--;
@SneakyThrows
private String currentGitBranch() {
try {
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.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import io.restassured.http.ContentType;
import lombok.Getter;
import lombok.SneakyThrows;
@ -33,6 +34,8 @@ import java.util.function.Function;
import java.util.function.Supplier;
import static java.net.URLEncoder.encode;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
import static net.hostsharing.hsadminng.hs.office.scenarios.TemplateResolver.Resolver.KEEP_COMMENTS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
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 String nextTitle; // just temporary to override resultAlias for sub-use-cases
private String introduction;
public UseCase(final ScenarioTest testSuite) {
this(testSuite, getResultAliasFromProducesAnnotationInCallStack());
@ -71,6 +75,9 @@ public abstract class UseCase<T extends UseCase<?>> {
}
public final HttpResponse doRun() {
if (introduction != null) {
testReport.printPara(introduction);
}
testReport.printPara("### Given Properties");
testReport.printLine("""
| name | value |
@ -81,7 +88,7 @@ public abstract class UseCase<T extends UseCase<?>> {
testReport.silent(() ->
requirements.forEach((alias, factory) -> {
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) {
}
public UseCase<T> introduction(final String introduction) {
this.introduction = introduction;
return this;
}
public final UseCase<T> given(final String propName, final Object propValue) {
givenProperties.put(propName, propValue);
ScenarioTest.putProperty(propName, propValue);
@ -106,11 +118,11 @@ public abstract class UseCase<T extends UseCase<?>> {
}
public final void obtain(
final String alias,
final String title,
final Supplier<HttpResponse> http,
final Function<HttpResponse, String> extractor,
final String... extraInfo) {
withTitle(ScenarioTest.resolve(alias), () -> {
withTitle(title, () -> {
final var response = http.get().keep(extractor);
Arrays.stream(extraInfo).forEach(testReport::printPara);
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) {
withTitle(ScenarioTest.resolve(alias), () -> {
withTitle(alias, () -> {
final var response = http.get().keep();
Arrays.stream(extraInfo).forEach(testReport::printPara);
return response;
});
}
public HttpResponse withTitle(final String title, final Supplier<HttpResponse> code) {
this.nextTitle = title;
public HttpResponse withTitle(final String resolvableTitle, final Supplier<HttpResponse> code) {
this.nextTitle = resolvableTitle;
final var response = code.get();
this.nextTitle = null;
return response;
@ -134,7 +146,7 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows
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()
.GET()
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
@ -147,7 +159,7 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows
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 request = HttpRequest.newBuilder()
.POST(BodyPublishers.ofString(requestBody))
@ -162,7 +174,7 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows
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 request = HttpRequest.newBuilder()
.method(HttpMethod.PATCH.toString(), BodyPublishers.ofString(requestBody))
@ -177,7 +189,7 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows
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()
.DELETE()
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
@ -197,7 +209,7 @@ public abstract class UseCase<T extends UseCase<?>> {
final String title,
final Supplier<UseCase.HttpResponse> http,
final Consumer<UseCase.HttpResponse>... assertions) {
withTitle(ScenarioTest.resolve(title), () -> {
withTitle(title, () -> {
final var response = http.get();
Arrays.stream(assertions).forEach(assertion -> assertion.accept(response));
return response;
@ -209,7 +221,7 @@ public abstract class UseCase<T extends UseCase<?>> {
}
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 {
@ -221,7 +233,7 @@ public abstract class UseCase<T extends UseCase<?>> {
}
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) {
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();
final var value = extractor.apply(this);
@ -276,15 +288,20 @@ public abstract class UseCase<T extends UseCase<?>> {
return this;
}
public HttpResponse keep() {
final var alias = nextTitle != null ? nextTitle : resultAlias;
assertThat(alias).as("cannot keep result, no alias found").isNotNull();
public HttpResponse keepAs(final String alias) {
ScenarioTest.putAlias(
alias,
nonNullAlias(alias),
new ScenarioTest.Alias<>(UseCase.this.getClass(), locationUuid));
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
public HttpResponse expectArrayElements(final int expectedElementCount) {
final var rootNode = objectMapper.readTree(response.body());
@ -298,20 +315,20 @@ public abstract class UseCase<T extends UseCase<?>> {
@SneakyThrows
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
public Optional<String> getFromBodyAsOptional(final String path) {
public <T> Optional<T> getFromBodyAsOptional(final String path) {
try {
return Optional.ofNullable(JsonPath.parse(response.body()).read(ScenarioTest.resolve(path)));
} catch (final Exception e) {
return Optional.ofNullable(JsonPath.parse(response.body()).read(ScenarioTest.resolve(path, DROP_COMMENTS)));
} catch (final PathNotFoundException e) {
return null; // means the property did not exist at all, not that it was there with value null
}
}
@SneakyThrows
public OptionalAssert<String> path(final String path) {
public <T> OptionalAssert<T> path(final String path) {
return assertThat(getFromBodyAsOptional(path));
}
@ -320,9 +337,9 @@ public abstract class UseCase<T extends UseCase<?>> {
// the title
if (nextTitle != null) {
testReport.printLine("\n### " + nextTitle + "\n");
testReport.printLine("\n### " + ScenarioTest.resolve(nextTitle, KEEP_COMMENTS) + "\n");
} else if (resultAlias != null) {
testReport.printLine("\n### " + resultAlias + "\n");
testReport.printLine("\n### Create " + resultAlias + "\n");
} else {
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("");
}
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() {

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.scenarios.membership;
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.UseCase;
import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON;
@ -16,10 +16,18 @@ public class CreateMembership extends UseCase<CreateMembership> {
@Override
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("""
{
"partner.uuid": ${Partner: Test AG},
"memberNumberSuffix": ${memberNumberSuffix},
"partner.uuid": ${Partner: %{partnerName}},
"memberNumberSuffix": ${%{memberNumberSuffix???}???00},
"status": "ACTIVE",
"validFrom": ${validFrom},
"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) {
super(testSuite);
introduction("A partner can be a client or a vendor, currently we only use them for clients.");
}
@Override