From e09a09cf929a03bd1e39e006d44e88e2d8693953 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 30 Apr 2024 12:27:20 +0200 Subject: [PATCH] office-related spec-clarifications and -amendmends (contact.emailaddresses+.phonenumbers JSON) (#50) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/50 Reviewed-by: Marc Sandlus --- .../hs/booking/item/HsBookingItemEntity.java | 24 +++--- .../hosting/asset/HsHostingAssetEntity.java | 16 ++-- .../contact/HsOfficeContactController.java | 13 ++- .../office/contact/HsOfficeContactEntity.java | 45 +++++++++-- ...java => HsOfficeContactEntityPatcher.java} | 13 ++- .../office/debitor/HsOfficeDebitorEntity.java | 2 +- .../relation/HsOfficeRelationEntity.java | 2 +- .../hsadminng/mapper/KeyValueMap.java | 6 +- .../hsadminng/mapper/PatchMap.java | 8 +- .../hsadminng/mapper/PatchableMapWrapper.java | 40 ++++++---- .../hsadminng/stringify/Stringify.java | 10 +++ .../hs-office/hs-office-contact-schemas.yaml | 39 +++++++-- .../changelog/1-rbac/1051-rbac-user-grant.sql | 2 +- .../1-rbac/1057-rbac-role-builder.sql | 2 +- .../501-contact/5010-hs-office-contact.sql | 4 +- .../5018-hs-office-contact-test-data.sql | 18 +++-- .../506-debitor/5060-hs-office-debitor.sql | 2 +- ...OfficeContactControllerAcceptanceTest.java | 53 +++++++----- .../HsOfficeContactEntityPatcherUnitTest.java | 60 +++++++++++--- .../office/contact/TestHsOfficeContact.java | 3 +- ...OfficeDebitorControllerAcceptanceTest.java | 38 +++++---- .../hs/office/migration/ImportOfficeData.java | 80 +++++++++---------- .../test/pac/TestPackageEntityUnitTest.java | 26 +++--- 23 files changed, 324 insertions(+), 182 deletions(-) rename src/main/java/net/hostsharing/hsadminng/hs/office/contact/{HsOfficeContactEntityPatch.java => HsOfficeContactEntityPatcher.java} (51%) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java index 8bdb5c8b..a0574bce 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -98,7 +98,15 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { private Map resources = new HashMap<>(); @Transient - private PatchableMapWrapper resourcesWrapper; + private PatchableMapWrapper resourcesWrapper; + + public PatchableMapWrapper getResources() { + return PatchableMapWrapper.of(resourcesWrapper, (newWrapper) -> {resourcesWrapper = newWrapper; }, resources ); + } + + public void putResources(Map newResources) { + getResources().assign(newResources); + } public void setValidFrom(final LocalDate validFrom) { setValidity(toPostgresDateRange(validFrom, getValidTo())); @@ -116,20 +124,6 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { return upperInclusiveFromPostgresDateRange(getValidity()); } - public PatchableMapWrapper getResources() { - if ( resourcesWrapper == null ) { - resourcesWrapper = new PatchableMapWrapper(resources); - } - return resourcesWrapper; - } - - public void putResources(Map entries) { - if ( resourcesWrapper == null ) { - resourcesWrapper = new PatchableMapWrapper(resources); - } - resourcesWrapper.assign(entries); - } - @Override public String toString() { return stringify.apply(this); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java index 199e0e7b..462ccd2c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java @@ -105,20 +105,14 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { private Map config = new HashMap<>(); @Transient - private PatchableMapWrapper configWrapper; + private PatchableMapWrapper configWrapper; - public PatchableMapWrapper getConfig() { - if ( configWrapper == null ) { - configWrapper = new PatchableMapWrapper(config); - } - return configWrapper; + public PatchableMapWrapper getConfig() { + return PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config ); } - public void putConfig(Map entries) { - if ( configWrapper == null ) { - configWrapper = new PatchableMapWrapper(config); - } - configWrapper.assign(entries); + public void putConfig(Map newConfg) { + PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfg); } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java index 073587f2..90449ce7 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java @@ -14,6 +14,9 @@ import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBui import java.util.List; import java.util.UUID; +import java.util.function.BiConsumer; + +import static net.hostsharing.hsadminng.mapper.KeyValueMap.from; @RestController @@ -51,7 +54,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi { context.define(currentUser, assumedRoles); - final var entityToSave = mapper.map(body, HsOfficeContactEntity.class); + final var entityToSave = mapper.map(body, HsOfficeContactEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); final var saved = contactRepo.save(entityToSave); @@ -108,10 +111,16 @@ public class HsOfficeContactController implements HsOfficeContactsApi { final var current = contactRepo.findByUuid(contactUuid).orElseThrow(); - new HsOfficeContactEntityPatch(current).apply(body); + new HsOfficeContactEntityPatcher(current).apply(body); final var saved = contactRepo.save(current); final var mapped = mapper.map(saved, HsOfficeContactResource.class); return ResponseEntity.ok(mapped); } + + @SuppressWarnings("unchecked") + final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { + entity.putEmailAddresses(from(resource.getEmailAddresses())); + entity.putPhoneNumbers(from(resource.getPhoneNumbers())); + }; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java index e09d0044..87caacfe 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java @@ -1,17 +1,22 @@ package net.hostsharing.hsadminng.hs.office.contact; +import io.hypersistence.utils.hibernate.type.json.JsonType; import lombok.*; import lombok.experimental.FieldNameConstants; import net.hostsharing.hsadminng.errors.DisplayName; +import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Type; import jakarta.persistence.*; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; @@ -44,17 +49,45 @@ public class HsOfficeContactEntity implements Stringifyable, RbacObject { @Version private int version; - @Column(name = "label") + @Column(name = "label") // TODO.impl: rename to caption private String label; @Column(name = "postaladdress") - private String postalAddress; // TODO.spec: check if we really want multiple, if so: JSON-Array or Postgres-Array? + private String postalAddress; // multiline free-format text - @Column(name = "emailaddresses", columnDefinition = "json") - private String emailAddresses; // TODO.spec: check if we can really add multiple. format: ["eins@...", "zwei@..."] + @Builder.Default + @Setter(AccessLevel.NONE) + @Type(JsonType.class) + @Column(name = "emailaddresses") + private Map emailAddresses = new HashMap<>(); - @Column(name = "phonenumbers", columnDefinition = "json") - private String phoneNumbers; // TODO.spec: check if we can really add multiple. format: { "office": "+49 40 12345-10", "fax": "+49 40 12345-05" } + @Transient + private PatchableMapWrapper emailAddressesWrapper; + + @Builder.Default + @Setter(AccessLevel.NONE) + @Type(JsonType.class) + @Column(name = "phonenumbers") + private Map phoneNumbers = new HashMap<>(); + + @Transient + private PatchableMapWrapper phoneNumbersWrapper; + + public PatchableMapWrapper getEmailAddresses() { + return PatchableMapWrapper.of(emailAddressesWrapper, (newWrapper) -> {emailAddressesWrapper = newWrapper; }, emailAddresses ); + } + + public void putEmailAddresses(Map newEmailAddresses) { + getEmailAddresses().assign(newEmailAddresses); + } + + public PatchableMapWrapper getPhoneNumbers() { + return PatchableMapWrapper.of(phoneNumbersWrapper, (newWrapper) -> {phoneNumbersWrapper = newWrapper; }, phoneNumbers ); + } + + public void putPhoneNumbers(Map newPhoneNumbers) { + getPhoneNumbers().assign(newPhoneNumbers); + } @Override public String toString() { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatch.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatcher.java similarity index 51% rename from src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatch.java rename to src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatcher.java index af6cfbc6..edefb8f3 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatch.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatcher.java @@ -1,14 +1,17 @@ package net.hostsharing.hsadminng.hs.office.contact; import net.hostsharing.hsadminng.mapper.EntityPatcher; +import net.hostsharing.hsadminng.mapper.KeyValueMap; import net.hostsharing.hsadminng.mapper.OptionalFromJson; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactPatchResource; -class HsOfficeContactEntityPatch implements EntityPatcher { +import java.util.Optional; + +class HsOfficeContactEntityPatcher implements EntityPatcher { private final HsOfficeContactEntity entity; - HsOfficeContactEntityPatch(final HsOfficeContactEntity entity) { + HsOfficeContactEntityPatcher(final HsOfficeContactEntity entity) { this.entity = entity; } @@ -16,7 +19,9 @@ class HsOfficeContactEntityPatch implements EntityPatcher entity.getEmailAddresses().patch(KeyValueMap.from(resource.getEmailAddresses()))); + Optional.ofNullable(resource.getPhoneNumbers()) + .ifPresent(r -> entity.getPhoneNumbers().patch(KeyValueMap.from(resource.getPhoneNumbers()))); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java index 33e6f2e8..9cf134c9 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java @@ -170,7 +170,7 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable { "vatCountryCode", "vatBusiness", "vatReverseCharge", - "defaultPrefix" /* TODO.spec: do we want that updatable? */) + "defaultPrefix") .toRole("global", ADMIN).grantPermission(INSERT) .importRootEntityAliasProxy("debitorRel", HsOfficeRelationEntity.class, usingCase(DEBITOR), diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java index 581e6bb7..7c8cd78e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java @@ -142,7 +142,7 @@ public class HsOfficeRelationEntity implements RbacObject, Stringifyable { with.permission(UPDATE); }) .createSubRole(AGENT, (with) -> { - // TODO.spec: we need relation:PROXY, to allow changing the relation contact. + // TODO.rbac: we need relation:PROXY, to allow changing the relation contact. // the alternative would be to move this to the relation:ADMIN role, // but then the partner holder person could update the partner relation itself, // see partner entity. diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/KeyValueMap.java b/src/main/java/net/hostsharing/hsadminng/mapper/KeyValueMap.java index 5a8cff2f..7fded816 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/KeyValueMap.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/KeyValueMap.java @@ -5,9 +5,9 @@ import java.util.Map; public class KeyValueMap { @SuppressWarnings("unchecked") - public static Map from(final Object obj) { - if (obj instanceof Map) { - return (Map) obj; + public static Map from(final Object obj) { + if (obj == null || obj instanceof Map) { + return (Map) obj; } throw new ClassCastException("Map expected, but got: " + obj); } diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java b/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java index 74a36bfa..6fd843c9 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java @@ -12,19 +12,19 @@ import static java.util.Arrays.stream; * This is a map which can take key-value-pairs where the value can be null * thus JSON nullable object structures from HTTP PATCH can be represented. */ -public class PatchMap extends TreeMap { +public class PatchMap extends TreeMap { - public PatchMap(final ImmutablePair[] entries) { + public PatchMap(final ImmutablePair[] entries) { stream(entries).forEach(r -> put(r.getKey(), r.getValue())); } @SafeVarargs - public static Map patchMap(final ImmutablePair... entries) { + public static Map patchMap(final ImmutablePair... entries) { return new PatchMap(entries); } @NotNull - public static ImmutablePair entry(final String key, final Object value) { + public static ImmutablePair entry(final String key, final T value) { return new ImmutablePair<>(key, value); } } diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java index 678a68cd..4962ac8d 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java @@ -6,31 +6,43 @@ import jakarta.validation.constraints.NotNull; import java.util.Collection; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; +import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.joining; /** This class wraps another (usually persistent) map and * supports applying `PatchMap` as well as a toString method with stable entry order. */ -public class PatchableMapWrapper implements Map { +public class PatchableMapWrapper implements Map { - private final Map delegate; + private final Map delegate; - public PatchableMapWrapper(final Map map) { + private PatchableMapWrapper(final Map map) { delegate = map; } + public static PatchableMapWrapper of(final PatchableMapWrapper currentWrapper, final Consumer> setWrapper, final Map target) { + return ofNullable(currentWrapper).orElseGet(() -> { + final var newWrapper = new PatchableMapWrapper(target); + setWrapper.accept(newWrapper); + return newWrapper; + }); + } + @NotNull - public static ImmutablePair entry(final String key, final Object value) { + public static ImmutablePair entry(final String key, final E value) { return new ImmutablePair<>(key, value); } - public void assign(final Map entries) { - delegate.clear(); - delegate.putAll(entries); + public void assign(final Map entries) { + if (entries != null ) { + delegate.clear(); + delegate.putAll(entries); + } } - public void patch(final Map patch) { + public void patch(final Map patch) { patch.forEach((key, value) -> { if (value == null) { remove(key); @@ -73,22 +85,22 @@ public class PatchableMapWrapper implements Map { } @Override - public Object get(final Object key) { + public T get(final Object key) { return delegate.get(key); } @Override - public Object put(final String key, final Object value) { + public T put(final String key, final T value) { return delegate.put(key, value); } @Override - public Object remove(final Object key) { + public T remove(final Object key) { return delegate.remove(key); } @Override - public void putAll(final Map m) { + public void putAll(final @NotNull Map m) { delegate.putAll(m); } @@ -103,12 +115,12 @@ public class PatchableMapWrapper implements Map { } @Override - public Collection values() { + public Collection values() { return delegate.values(); } @Override - public Set> entrySet() { + public Set> entrySet() { return delegate.entrySet(); } } diff --git a/src/main/java/net/hostsharing/hsadminng/stringify/Stringify.java b/src/main/java/net/hostsharing/hsadminng/stringify/Stringify.java index 8cdf433b..ffdb7a5a 100644 --- a/src/main/java/net/hostsharing/hsadminng/stringify/Stringify.java +++ b/src/main/java/net/hostsharing/hsadminng/stringify/Stringify.java @@ -4,7 +4,9 @@ import net.hostsharing.hsadminng.errors.DisplayName; import jakarta.validation.constraints.NotNull; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Function; @@ -62,6 +64,7 @@ public final class Stringify { final var propValues = props.stream() .map(prop -> PropertyValue.of(prop, prop.getter.apply(object))) .filter(Objects::nonNull) + .filter(PropertyValue::nonEmpty) .map(propVal -> { if (propVal.rawValue instanceof Stringifyable stringifyable) { return new PropertyValue<>(propVal.prop, propVal.rawValue, stringifyable.toShortString()); @@ -110,5 +113,12 @@ public final class Stringify { static PropertyValue of(Property prop, Object rawValue) { return rawValue != null ? new PropertyValue<>(prop, rawValue, rawValue.toString()) : null; } + + boolean nonEmpty() { + return rawValue != null && + (!(rawValue instanceof Collection c) || !c.isEmpty()) && + (!(rawValue instanceof Map m) || !m.isEmpty()) && + (!(rawValue instanceof String s) || !s.isEmpty()); + } } } diff --git a/src/main/resources/api-definition/hs-office/hs-office-contact-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-contact-schemas.yaml index 9d8dc76a..5905bdf4 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-contact-schemas.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-contact-schemas.yaml @@ -14,9 +14,9 @@ components: postalAddress: type: string emailAddresses: - type: string + $ref: '#/components/schemas/HsOfficeContactEmailAddresses' phoneNumbers: - type: string + $ref: '#/components/schemas/HsOfficeContactPhoneNumbers' HsOfficeContactInsert: type: object @@ -26,9 +26,9 @@ components: postalAddress: type: string emailAddresses: - type: string + $ref: '#/components/schemas/HsOfficeContactEmailAddresses' phoneNumbers: - type: string + $ref: '#/components/schemas/HsOfficeContactPhoneNumbers' required: - label @@ -42,8 +42,31 @@ components: type: string nullable: true emailAddresses: - type: string - nullable: true + $ref: '#/components/schemas/HsOfficeContactEmailAddresses' phoneNumbers: - type: string - nullable: true + $ref: '#/components/schemas/HsOfficeContactPhoneNumbers' + + HsOfficeContactEmailAddresses: + # forces generating a java.lang.Object containing a Map, instead of class HsOfficeContactEmailAddresses + anyOf: + - type: object + additionalProperties: true + + HsOfficeContactPhoneNumbers: + # forces generating a java.lang.Object containing a Map, instead of class HsOfficeContactEmailAddresses + anyOf: + - type: object + properties: + phone_office: + type: string + nullable: true + phone_private: + type: string + nullable: true + phone_mobile: + type: string + nullable: true + fax: + type: string + nullable: true + additionalProperties: false diff --git a/src/main/resources/db/changelog/1-rbac/1051-rbac-user-grant.sql b/src/main/resources/db/changelog/1-rbac/1051-rbac-user-grant.sql index 17645ca3..fc74a6de 100644 --- a/src/main/resources/db/changelog/1-rbac/1051-rbac-user-grant.sql +++ b/src/main/resources/db/changelog/1-rbac/1051-rbac-user-grant.sql @@ -63,7 +63,7 @@ begin insert into RbacGrants (grantedByRoleUuid, ascendantUuid, descendantUuid, assumed) values (grantedByRoleUuid, userUuid, grantedRoleUuid, doAssume); - -- TODO.spec: What should happen on mupltiple grants? What if options (doAssume) are not the same? + -- TODO.impl: What should happen on mupltiple grants? What if options (doAssume) are not the same? -- Most powerful or latest grant wins? What about managed? -- on conflict do nothing; -- allow granting multiple times end; $$; diff --git a/src/main/resources/db/changelog/1-rbac/1057-rbac-role-builder.sql b/src/main/resources/db/changelog/1-rbac/1057-rbac-role-builder.sql index 1e9bd2bc..cb20bbbc 100644 --- a/src/main/resources/db/changelog/1-rbac/1057-rbac-role-builder.sql +++ b/src/main/resources/db/changelog/1-rbac/1057-rbac-role-builder.sql @@ -52,7 +52,7 @@ begin if cardinality(userUuids) > 0 then -- direct grants to users need a grantedByRole which can revoke the grant if grantedByRole is null then - userGrantsByRoleUuid := roleUuid; -- TODO.spec: or do we want to require an explicit userGrantsByRoleUuid? + userGrantsByRoleUuid := roleUuid; -- TODO.impl: or do we want to require an explicit userGrantsByRoleUuid? else userGrantsByRoleUuid := getRoleId(grantedByRole); end if; diff --git a/src/main/resources/db/changelog/5-hs-office/501-contact/5010-hs-office-contact.sql b/src/main/resources/db/changelog/5-hs-office/501-contact/5010-hs-office-contact.sql index d6428651..ca875a89 100644 --- a/src/main/resources/db/changelog/5-hs-office/501-contact/5010-hs-office-contact.sql +++ b/src/main/resources/db/changelog/5-hs-office/501-contact/5010-hs-office-contact.sql @@ -10,8 +10,8 @@ create table if not exists hs_office_contact version int not null default 0, label varchar(128) not null, postalAddress text, - emailAddresses text, -- TODO.feat: change to json - phoneNumbers text -- TODO.feat: change to json + emailAddresses jsonb not null, + phoneNumbers jsonb not null ); --// diff --git a/src/main/resources/db/changelog/5-hs-office/501-contact/5018-hs-office-contact-test-data.sql b/src/main/resources/db/changelog/5-hs-office/501-contact/5018-hs-office-contact-test-data.sql index 7970e0f6..e9e7a9e0 100644 --- a/src/main/resources/db/changelog/5-hs-office/501-contact/5018-hs-office-contact-test-data.sql +++ b/src/main/resources/db/changelog/5-hs-office/501-contact/5018-hs-office-contact-test-data.sql @@ -11,8 +11,9 @@ create or replace procedure createHsOfficeContactTestData(contLabel varchar) language plpgsql as $$ declare - currentTask varchar; - emailAddr varchar; + currentTask varchar; + postalAddr varchar; + emailAddr varchar; begin currentTask = 'creating contact test-data ' || contLabel; execute format('set local hsadminng.currentTask to %L', currentTask); @@ -22,14 +23,17 @@ begin perform createRbacUser(emailAddr); call defineContext(currentTask, null, emailAddr); + postalAddr := E'Vorname Nachname\nStraße Hnr\nPLZ Stadt'; + raise notice 'creating test contact: %', contLabel; insert into hs_office_contact (label, postaladdress, emailaddresses, phonenumbers) - values (contLabel, $address$ -Vorname Nachname -Straße Hnr -PLZ Stadt -$address$, emailAddr, '+49 123 1234567'); + values ( + contLabel, + postalAddr, + ('{ "main": "' || emailAddr || '" }')::jsonb, + ('{ "phone_office": "+49 123 1234567" }')::jsonb + ); end; $$; --// diff --git a/src/main/resources/db/changelog/5-hs-office/506-debitor/5060-hs-office-debitor.sql b/src/main/resources/db/changelog/5-hs-office/506-debitor/5060-hs-office-debitor.sql index 39db61e2..bbf72543 100644 --- a/src/main/resources/db/changelog/5-hs-office/506-debitor/5060-hs-office-debitor.sql +++ b/src/main/resources/db/changelog/5-hs-office/506-debitor/5060-hs-office-debitor.sql @@ -11,7 +11,7 @@ create table hs_office_debitor debitorNumberSuffix char(2) not null check (debitorNumberSuffix::text ~ '^[0-9][0-9]$'), debitorRelUuid uuid not null references hs_office_relation(uuid), billable boolean not null default true, - vatId varchar(24), -- TODO.spec: here or in person? + vatId varchar(24), vatCountryCode varchar(2), vatBusiness boolean not null, vatReverseCharge boolean not null, diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactControllerAcceptanceTest.java index c7833c9c..1b209737 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactControllerAcceptanceTest.java @@ -19,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import java.util.Map; import java.util.UUID; import static net.hostsharing.hsadminng.rbac.test.IsValidUuidMatcher.isUuidValid; @@ -103,7 +104,9 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu .body(""" { "label": "Temp Contact", - "emailAddresses": "test@example.org" + "emailAddresses": { + "main": "test@example.org" + } } """) .port(port) @@ -114,7 +117,7 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu .contentType(ContentType.JSON) .body("uuid", isUuidValid()) .body("label", is("Temp Contact")) - .body("emailAddresses", is("test@example.org")) + .body("emailAddresses", is(Map.of("main", "test@example.org"))) .header("Location", startsWith("http://localhost")) .extract().header("Location"); // @formatter:on @@ -180,9 +183,13 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu .contentType("application/json") .body("", lenientlyEquals(""" { - "label": "first contact", - "emailAddresses": "contact-admin@firstcontact.example.com", - "phoneNumbers": "+49 123 1234567" + "label": "first contact", + "emailAddresses": { + "main": "contact-admin@firstcontact.example.com" + }, + "phoneNumbers": { + "phone_office": "+49 123 1234567" + } } """)); // @formatter:on } @@ -204,9 +211,13 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu .body(""" { "label": "Temp patched contact", - "emailAddresses": "patched@example.org", + "emailAddresses": { + "main": "patched@example.org" + }, "postalAddress": "Patched Address", - "phoneNumbers": "+01 100 123456" + "phoneNumbers": { + "phone_office": "+01 100 123456" + } } """) .port(port) @@ -217,9 +228,9 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu .contentType(ContentType.JSON) .body("uuid", isUuidValid()) .body("label", is("Temp patched contact")) - .body("emailAddresses", is("patched@example.org")) + .body("emailAddresses", is(Map.of("main", "patched@example.org"))) .body("postalAddress", is("Patched Address")) - .body("phoneNumbers", is("+01 100 123456")); + .body("phoneNumbers", is(Map.of("phone_office", "+01 100 123456"))); // @formatter:on // finally, the contact is actually updated @@ -227,9 +238,9 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu assertThat(contactRepo.findByUuid(givenContact.getUuid())).isPresent().get() .matches(person -> { assertThat(person.getLabel()).isEqualTo("Temp patched contact"); - assertThat(person.getEmailAddresses()).isEqualTo("patched@example.org"); + assertThat(person.getEmailAddresses()).containsExactlyEntriesOf(Map.of("main", "patched@example.org")); assertThat(person.getPostalAddress()).isEqualTo("Patched Address"); - assertThat(person.getPhoneNumbers()).isEqualTo("+01 100 123456"); + assertThat(person.getPhoneNumbers()).containsExactlyEntriesOf(Map.of("phone_office", "+01 100 123456")); return true; }); } @@ -246,8 +257,12 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu .contentType(ContentType.JSON) .body(""" { - "emailAddresses": "patched@example.org", - "phoneNumbers": "+01 100 123456" + "emailAddresses": { + "main": "patched@example.org" + }, + "phoneNumbers": { + "phone_office": "+01 100 123456" + } } """) .port(port) @@ -258,18 +273,18 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu .contentType(ContentType.JSON) .body("uuid", isUuidValid()) .body("label", is(givenContact.getLabel())) - .body("emailAddresses", is("patched@example.org")) + .body("emailAddresses", is(Map.of("main", "patched@example.org"))) .body("postalAddress", is(givenContact.getPostalAddress())) - .body("phoneNumbers", is("+01 100 123456")); + .body("phoneNumbers", is(Map.of("phone_office", "+01 100 123456"))); // @formatter:on // finally, the contact is actually updated assertThat(contactRepo.findByUuid(givenContact.getUuid())).isPresent().get() .matches(person -> { assertThat(person.getLabel()).isEqualTo(givenContact.getLabel()); - assertThat(person.getEmailAddresses()).isEqualTo("patched@example.org"); + assertThat(person.getEmailAddresses()).containsExactlyEntriesOf(Map.of("main", "patched@example.org")); assertThat(person.getPostalAddress()).isEqualTo(givenContact.getPostalAddress()); - assertThat(person.getPhoneNumbers()).isEqualTo("+01 100 123456"); + assertThat(person.getPhoneNumbers()).containsExactlyEntriesOf(Map.of("phone_office", "+01 100 123456")); return true; }); } @@ -340,9 +355,9 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu final var newContact = HsOfficeContactEntity.builder() .uuid(UUID.randomUUID()) .label("Temp from " + Context.getCallerMethodNameFromStackFrame(1) ) - .emailAddresses(RandomStringUtils.randomAlphabetic(10) + "@example.org") + .emailAddresses(Map.of("main", RandomStringUtils.randomAlphabetic(10) + "@example.org")) .postalAddress("Postal Address " + RandomStringUtils.randomAlphabetic(10)) - .phoneNumbers("+01 200 " + RandomStringUtils.randomNumeric(8)) + .phoneNumbers(Map.of("phone_office", "+01 200 " + RandomStringUtils.randomNumeric(8))) .build(); return contactRepo.save(newContact); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatcherUnitTest.java index 31a5ca02..a9c20958 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntityPatcherUnitTest.java @@ -4,9 +4,12 @@ import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactPatchResource; import org.junit.jupiter.api.TestInstance; +import java.util.Map; import java.util.UUID; import java.util.stream.Stream; +import static net.hostsharing.hsadminng.mapper.PatchMap.entry; +import static net.hostsharing.hsadminng.mapper.PatchMap.patchMap; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; @TestInstance(PER_CLASS) @@ -16,15 +19,42 @@ class HsOfficeContactEntityPatcherUnitTest extends PatchUnitTestBase< > { private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID(); + private static final Map PATCH_EMAIL_ADDRESSES = patchMap( + entry("main", "patched@example.com"), + entry("paul", null), + entry("suse", "suse@example.com") + ); + private static final Map PATCHED_EMAIL_ADDRESSES = patchMap( + entry("main", "patched@example.com"), + entry("suse", "suse@example.com"), + entry("mila", "mila@example.com") + ); + + private static final Map PATCH_PHONE_NUMBERS = patchMap( + entry("phone_mobile", null), + entry("phone_private", "+49 40 987654321"), + entry("fax", "+49 40 12345-99") + ); + private static final Map PATCHED_PHONE_NUMBERS = patchMap( + entry("phone_office", "+49 40 12345-00"), + entry("phone_private", "+49 40 987654321"), + entry("fax", "+49 40 12345-99") + ); @Override protected HsOfficeContactEntity newInitialEntity() { final var entity = new HsOfficeContactEntity(); entity.setUuid(INITIAL_CONTACT_UUID); entity.setLabel("initial label"); - entity.setEmailAddresses("initial@example.org"); - entity.setPhoneNumbers("initial postal address"); - entity.setPostalAddress("+01 100 123456789"); + entity.putEmailAddresses(Map.ofEntries( + entry("main", "initial@example.org"), + entry("paul", "paul@example.com"), + entry("mila", "mila@example.com"))); + entity.putPhoneNumbers(Map.ofEntries( + entry("phone_office", "+49 40 12345-00"), + entry("phone_mobile", "+49 1555 1234567"), + entry("fax", "+49 40 12345-90"))); + entity.setPostalAddress("Initialstraße 50\n20000 Hamburg"); return entity; } @@ -34,8 +64,8 @@ class HsOfficeContactEntityPatcherUnitTest extends PatchUnitTestBase< } @Override - protected HsOfficeContactEntityPatch createPatcher(final HsOfficeContactEntity entity) { - return new HsOfficeContactEntityPatch(entity); + protected HsOfficeContactEntityPatcher createPatcher(final HsOfficeContactEntity entity) { + return new HsOfficeContactEntityPatcher(entity); } @Override @@ -46,16 +76,20 @@ class HsOfficeContactEntityPatcherUnitTest extends PatchUnitTestBase< HsOfficeContactPatchResource::setLabel, "patched label", HsOfficeContactEntity::setLabel), - new JsonNullableProperty<>( - "emailAddresses", + new SimpleProperty<>( + "resources", HsOfficeContactPatchResource::setEmailAddresses, - "patched trade name", - HsOfficeContactEntity::setEmailAddresses), - new JsonNullableProperty<>( - "phoneNumbers", + PATCH_EMAIL_ADDRESSES, + HsOfficeContactEntity::putEmailAddresses, + PATCHED_EMAIL_ADDRESSES) + .notNullable(), + new SimpleProperty<>( + "resources", HsOfficeContactPatchResource::setPhoneNumbers, - "patched family name", - HsOfficeContactEntity::setPhoneNumbers), + PATCH_PHONE_NUMBERS, + HsOfficeContactEntity::putPhoneNumbers, + PATCHED_PHONE_NUMBERS) + .notNullable(), new JsonNullableProperty<>( "patched given name", HsOfficeContactPatchResource::setPostalAddress, diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/TestHsOfficeContact.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/TestHsOfficeContact.java index b42ef8e5..9256084f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/TestHsOfficeContact.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/TestHsOfficeContact.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.office.contact; +import java.util.Map; public class TestHsOfficeContact { @@ -9,7 +10,7 @@ public class TestHsOfficeContact { return HsOfficeContactEntity.builder() .label(label) .postalAddress("address of " + label) - .emailAddresses(emailAddr) + .emailAddresses(Map.of("main", emailAddr)) .build(); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java index 9b3638c4..9bda7ec4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java @@ -107,8 +107,8 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu "mark": null, "contact": { "label": "first contact", - "emailAddresses": "contact-admin@firstcontact.example.com", - "phoneNumbers": "+49 123 1234567" + "emailAddresses": { "main": "contact-admin@firstcontact.example.com" }, + "phoneNumbers": { "phone_office": "+49 123 1234567" } } }, "debitorNumber": 1000111, @@ -132,8 +132,8 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu "mark": null, "contact": { "label": "first contact", - "emailAddresses": "contact-admin@firstcontact.example.com", - "phoneNumbers": "+49 123 1234567" + "emailAddresses": { "main": "contact-admin@firstcontact.example.com" }, + "phoneNumbers": { "phone_office": "+49 123 1234567" } } }, "details": { @@ -162,7 +162,9 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu "anchor": {"tradeName": "Second e.K."}, "holder": {"tradeName": "Second e.K."}, "type": "DEBITOR", - "contact": {"emailAddresses": "contact-admin@secondcontact.example.com"} + "contact": { + "emailAddresses": { "main": "contact-admin@secondcontact.example.com" } + } }, "debitorNumber": 1000212, "debitorNumberSuffix": 12, @@ -172,7 +174,9 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu "anchor": {"tradeName": "Hostsharing eG"}, "holder": {"tradeName": "Second e.K."}, "type": "PARTNER", - "contact": {"emailAddresses": "contact-admin@secondcontact.example.com"} + "contact": { + "emailAddresses": { "main": "contact-admin@secondcontact.example.com" } + } }, "details": { "registrationOffice": "Hamburg", @@ -192,7 +196,9 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu "anchor": {"tradeName": "Third OHG"}, "holder": {"tradeName": "Third OHG"}, "type": "DEBITOR", - "contact": {"emailAddresses": "contact-admin@thirdcontact.example.com"} + "contact": { + "emailAddresses": { "main": "contact-admin@thirdcontact.example.com" } + } }, "debitorNumber": 1000313, "debitorNumberSuffix": 13, @@ -202,7 +208,9 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu "anchor": {"tradeName": "Hostsharing eG"}, "holder": {"tradeName": "Third OHG"}, "type": "PARTNER", - "contact": {"emailAddresses": "contact-admin@thirdcontact.example.com"} + "contact": { + "emailAddresses": { "main": "contact-admin@thirdcontact.example.com" } + } }, "details": { "registrationOffice": "Hamburg", @@ -223,7 +231,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu } @Test - void globalAdmin_withoutAssumedRoles_canFindDebitorDebitorByDebitorNumber() throws JSONException { + void globalAdmin_withoutAssumedRoles_canFindDebitorDebitorByDebitorNumber() { RestAssured // @formatter:off .given() @@ -456,9 +464,9 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu "type": "DEBITOR", "contact": { "label": "first contact", - "postalAddress": "\\nVorname Nachname\\nStraße Hnr\\nPLZ Stadt\\n", - "emailAddresses": "contact-admin@firstcontact.example.com", - "phoneNumbers": "+49 123 1234567" + "postalAddress": "Vorname Nachname\\nStraße Hnr\\nPLZ Stadt", + "emailAddresses": { "main": "contact-admin@firstcontact.example.com" }, + "phoneNumbers": { "phone_office": "+49 123 1234567" } } }, "debitorNumber": 1000111, @@ -472,9 +480,9 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu "mark": null, "contact": { "label": "first contact", - "postalAddress": "\\nVorname Nachname\\nStraße Hnr\\nPLZ Stadt\\n", - "emailAddresses": "contact-admin@firstcontact.example.com", - "phoneNumbers": "+49 123 1234567" + "postalAddress": "Vorname Nachname\\nStraße Hnr\\nPLZ Stadt", + "emailAddresses": { "main": "contact-admin@firstcontact.example.com" }, + "phoneNumbers": { "phone_office": "+49 123 1234567" } } }, "details": { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java index 3bc64cd1..9bb44f3d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java @@ -240,30 +240,30 @@ public class ImportOfficeData extends ContextBasedTest { """); assertThat(toFormattedString(contacts)).isEqualToIgnoringWhitespace(""" { - 1101=contact(label='Herr Michael Mellies ', emailAddresses='mih@example.org'), - 1200=contact(label='JM e.K.', emailAddresses='jm-ex-partner@example.org'), - 1201=contact(label='Frau Dr. Jenny Meyer-Billing , JM GmbH', emailAddresses='jm-billing@example.org'), - 1202=contact(label='Herr Andrew Meyer-Operation , JM GmbH', emailAddresses='am-operation@example.org'), - 1203=contact(label='Herr Philip Meyer-Contract , JM GmbH', emailAddresses='pm-partner@example.org'), - 1204=contact(label='Frau Tammy Meyer-VIP , JM GmbH', emailAddresses='tm-vip@example.org'), - 1301=contact(label='Petra Schmidt , Test PS', emailAddresses='ps@example.com'), - 1401=contact(label='Frau Frauke Fanninga ', emailAddresses='ff@example.org'), - 1501=contact(label='Frau Cecilia Camus ', emailAddresses='cc@example.org') + 1101=contact(label='Herr Michael Mellies ', emailAddresses='{ main: mih@example.org }'), + 1200=contact(label='JM e.K.', emailAddresses='{ main: jm-ex-partner@example.org }'), + 1201=contact(label='Frau Dr. Jenny Meyer-Billing , JM GmbH', emailAddresses='{ main: jm-billing@example.org }'), + 1202=contact(label='Herr Andrew Meyer-Operation , JM GmbH', emailAddresses='{ main: am-operation@example.org }'), + 1203=contact(label='Herr Philip Meyer-Contract , JM GmbH', emailAddresses='{ main: pm-partner@example.org }'), + 1204=contact(label='Frau Tammy Meyer-VIP , JM GmbH', emailAddresses='{ main: tm-vip@example.org }'), + 1301=contact(label='Petra Schmidt , Test PS', emailAddresses='{ main: ps@example.com }'), + 1401=contact(label='Frau Frauke Fanninga ', emailAddresses='{ main: ff@example.org }'), + 1501=contact(label='Frau Cecilia Camus ', emailAddresses='{ main: cc@example.org }') } """); assertThat(toFormattedString(persons)).isEqualToIgnoringWhitespace(""" { 1=person(personType='LP', tradeName='Hostsharing eG'), - 1101=person(personType='NP', tradeName='', familyName='Mellies', givenName='Michael'), - 1200=person(personType='LP', tradeName='JM e.K.', familyName='', givenName=''), + 1101=person(personType='NP', familyName='Mellies', givenName='Michael'), + 1200=person(personType='LP', tradeName='JM e.K.'), 1201=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Billing', givenName='Jenny'), 1202=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Operation', givenName='Andrew'), 1203=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Contract', givenName='Philip'), 1204=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-VIP', givenName='Tammy'), 1301=person(personType='??', tradeName='Test PS', familyName='Schmidt', givenName='Petra'), - 1401=person(personType='NP', tradeName='', familyName='Fanninga', givenName='Frauke'), - 1501=person(personType='NP', tradeName='', familyName='Camus', givenName='Cecilia') - } + 1401=person(personType='NP', familyName='Fanninga', givenName='Frauke'), + 1501=person(personType='NP', familyName='Camus', givenName='Cecilia') + } """); assertThat(toFormattedString(debitors)).isEqualToIgnoringWhitespace(""" { @@ -363,11 +363,11 @@ public class ImportOfficeData extends ContextBasedTest { assertThat(toFormattedString(coopShares)).isEqualToIgnoringWhitespace(""" { - 33443=CoopShareTransaction(M-1001700: 2000-12-06, SUBSCRIPTION, 20, legacy data import, initial share subscription), - 33451=CoopShareTransaction(M-1002000: 2000-12-06, SUBSCRIPTION, 2, legacy data import, initial share subscription), - 33701=CoopShareTransaction(M-1001700: 2005-01-10, SUBSCRIPTION, 40, legacy data import, increase), - 33810=CoopShareTransaction(M-1002000: 2016-12-31, CANCELLATION, 22, legacy data import, membership ended) - } + 33443=CoopShareTransaction(M-1001700: 2000-12-06, SUBSCRIPTION, 20, 1001700, initial share subscription), + 33451=CoopShareTransaction(M-1002000: 2000-12-06, SUBSCRIPTION, 2, 1002000, initial share subscription), + 33701=CoopShareTransaction(M-1001700: 2005-01-10, SUBSCRIPTION, 40, 1001700, increase), + 33810=CoopShareTransaction(M-1002000: 2016-12-31, CANCELLATION, 22, 1002000, membership ended) + } """); } @@ -390,16 +390,16 @@ public class ImportOfficeData extends ContextBasedTest { assertThat(toFormattedString(coopAssets)).isEqualToIgnoringWhitespace(""" { - 30000=CoopAssetsTransaction(M-1001700: 2000-12-06, DEPOSIT, 1280.00, legacy data import, for subscription A), - 31000=CoopAssetsTransaction(M-1002000: 2000-12-06, DEPOSIT, 128.00, legacy data import, for subscription B), - 32000=CoopAssetsTransaction(M-1001700: 2005-01-10, DEPOSIT, 2560.00, legacy data import, for subscription C), - 33001=CoopAssetsTransaction(M-1001700: 2005-01-10, TRANSFER, -512.00, legacy data import, for transfer to 10), - 33002=CoopAssetsTransaction(M-1002000: 2005-01-10, ADOPTION, 512.00, legacy data import, for transfer from 7), - 34001=CoopAssetsTransaction(M-1002000: 2016-12-31, CLEARING, -8.00, legacy data import, for cancellation D), - 34002=CoopAssetsTransaction(M-1002000: 2016-12-31, DISBURSAL, -100.00, legacy data import, for cancellation D), - 34003=CoopAssetsTransaction(M-1002000: 2016-12-31, LOSS, -20.00, legacy data import, for cancellation D), - 35001=CoopAssetsTransaction(M-1909000: 2024-01-15, DEPOSIT, 128.00, legacy data import, for subscription E), - 35002=CoopAssetsTransaction(M-1909000: 2024-01-20, ADJUSTMENT, -128.00, legacy data import, chargeback for subscription E, M-1909000:DEP:+128.00) + 30000=CoopAssetsTransaction(M-1001700: 2000-12-06, DEPOSIT, 1280.00, 1001700, for subscription A), + 31000=CoopAssetsTransaction(M-1002000: 2000-12-06, DEPOSIT, 128.00, 1002000, for subscription B), + 32000=CoopAssetsTransaction(M-1001700: 2005-01-10, DEPOSIT, 2560.00, 1001700, for subscription C), + 33001=CoopAssetsTransaction(M-1001700: 2005-01-10, TRANSFER, -512.00, 1001700, for transfer to 10), + 33002=CoopAssetsTransaction(M-1002000: 2005-01-10, ADOPTION, 512.00, 1002000, for transfer from 7), + 34001=CoopAssetsTransaction(M-1002000: 2016-12-31, CLEARING, -8.00, 1002000, for cancellation D), + 34002=CoopAssetsTransaction(M-1002000: 2016-12-31, DISBURSAL, -100.00, 1002000, for cancellation D), + 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) } """); } @@ -810,7 +810,7 @@ public class ImportOfficeData extends ContextBasedTest { ) .shareCount(rec.getInteger("quantity")) .comment( rec.getString("comment")) - .reference("legacy data import") // TODO.spec: or use value from comment column? + .reference(member.getMemberNumber().toString()) .build(); if (shareTransaction.getTransactionType() == HsOfficeCoopSharesTransactionType.ADJUSTMENT) { @@ -867,7 +867,7 @@ public class ImportOfficeData extends ContextBasedTest { .transactionType(assetTypeMapping.get(rec.getString("action"))) .assetValue(rec.getBigDecimal("amount")) .comment(rec.getString("comment")) - .reference("legacy data import") // TODO.spec: or use value from comment column? + .reference(member.getMemberNumber().toString()) .build(); if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.ADJUSTMENT) { @@ -1092,9 +1092,9 @@ public class ImportOfficeData extends ContextBasedTest { contactRecord.getString("first_name"), contactRecord.getString("last_name"), contactRecord.getString("firma"))); - contact.setEmailAddresses(contactRecord.getString("email")); + contact.putEmailAddresses( Map.of("main", contactRecord.getString("email"))); contact.setPostalAddress(toAddress(contactRecord)); - contact.setPhoneNumbers(toPhoneNumbers(contactRecord)); + contact.putPhoneNumbers(toPhoneNumbers(contactRecord)); contacts.put(contactRecord.getInteger("contact_id"), contact); return contact; @@ -1120,17 +1120,17 @@ public class ImportOfficeData extends ContextBasedTest { return record; } - private String toPhoneNumbers(final Record rec) { - final var result = new StringBuilder("{\n"); + private Map toPhoneNumbers(final Record rec) { + final var phoneNumbers = new LinkedHashMap(); if (isNotBlank(rec.getString("phone_private"))) - result.append(" \"private\": " + "\"" + rec.getString("phone_private") + "\",\n"); + phoneNumbers.put("phone_private", rec.getString("phone_private")); if (isNotBlank(rec.getString("phone_office"))) - result.append(" \"office\": " + "\"" + rec.getString("phone_office") + "\",\n"); + phoneNumbers.put("phone_office", rec.getString("phone_office")); if (isNotBlank(rec.getString("phone_mobile"))) - result.append(" \"mobile\": " + "\"" + rec.getString("phone_mobile") + "\",\n"); + phoneNumbers.put("phone_mobile", rec.getString("phone_mobile")); if (isNotBlank(rec.getString("fax"))) - result.append(" \"fax\": " + "\"" + rec.getString("fax") + "\",\n"); - return (result + "}").replace("\",\n}", "\"\n}"); + phoneNumbers.put("fax", rec.getString("fax")); + return phoneNumbers; } private String toAddress(final Record rec) { diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageEntityUnitTest.java index 660ad955..824bb1bb 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageEntityUnitTest.java @@ -13,6 +13,19 @@ class TestPackageEntityUnitTest { assertThat(rbacFlowchart).isEqualTo(""" %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB + + subgraph customer["`**customer**`"] + direction TB + style customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph customer:roles[ ] + style customer:roles fill:#99bcdb,stroke:white + + role:customer:OWNER[[customer:OWNER]] + role:customer:ADMIN[[customer:ADMIN]] + role:customer:TENANT[[customer:TENANT]] + end + end subgraph package["`**package**`"] direction TB @@ -36,19 +49,6 @@ class TestPackageEntityUnitTest { end end - subgraph customer["`**customer**`"] - direction TB - style customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px - - subgraph customer:roles[ ] - style customer:roles fill:#99bcdb,stroke:white - - role:customer:OWNER[[customer:OWNER]] - role:customer:ADMIN[[customer:ADMIN]] - role:customer:TENANT[[customer:TENANT]] - end - end - %% granting roles to roles role:global:ADMIN -.->|XX| role:customer:OWNER role:customer:OWNER -.-> role:customer:ADMIN