office-related spec-clarifications and -amendmends (contact.emailaddresses+.phonenumbers JSON) (#50)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #50
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-04-30 12:27:20 +02:00
parent dbe695c214
commit e09a09cf92
23 changed files with 324 additions and 182 deletions

View File

@ -98,7 +98,15 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
private Map<String, Object> resources = new HashMap<>(); private Map<String, Object> resources = new HashMap<>();
@Transient @Transient
private PatchableMapWrapper resourcesWrapper; private PatchableMapWrapper<Object> resourcesWrapper;
public PatchableMapWrapper<Object> getResources() {
return PatchableMapWrapper.of(resourcesWrapper, (newWrapper) -> {resourcesWrapper = newWrapper; }, resources );
}
public void putResources(Map<String, Object> newResources) {
getResources().assign(newResources);
}
public void setValidFrom(final LocalDate validFrom) { public void setValidFrom(final LocalDate validFrom) {
setValidity(toPostgresDateRange(validFrom, getValidTo())); setValidity(toPostgresDateRange(validFrom, getValidTo()));
@ -116,20 +124,6 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
return upperInclusiveFromPostgresDateRange(getValidity()); return upperInclusiveFromPostgresDateRange(getValidity());
} }
public PatchableMapWrapper getResources() {
if ( resourcesWrapper == null ) {
resourcesWrapper = new PatchableMapWrapper(resources);
}
return resourcesWrapper;
}
public void putResources(Map<String, Object> entries) {
if ( resourcesWrapper == null ) {
resourcesWrapper = new PatchableMapWrapper(resources);
}
resourcesWrapper.assign(entries);
}
@Override @Override
public String toString() { public String toString() {
return stringify.apply(this); return stringify.apply(this);

View File

@ -105,20 +105,14 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
private Map<String, Object> config = new HashMap<>(); private Map<String, Object> config = new HashMap<>();
@Transient @Transient
private PatchableMapWrapper configWrapper; private PatchableMapWrapper<Object> configWrapper;
public PatchableMapWrapper getConfig() { public PatchableMapWrapper<Object> getConfig() {
if ( configWrapper == null ) { return PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config );
configWrapper = new PatchableMapWrapper(config);
}
return configWrapper;
} }
public void putConfig(Map<String, Object> entries) { public void putConfig(Map<String, Object> newConfg) {
if ( configWrapper == null ) { PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfg);
configWrapper = new PatchableMapWrapper(config);
}
configWrapper.assign(entries);
} }
@Override @Override

View File

@ -14,6 +14,9 @@ import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBui
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
@RestController @RestController
@ -51,7 +54,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
context.define(currentUser, assumedRoles); 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); final var saved = contactRepo.save(entityToSave);
@ -108,10 +111,16 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
final var current = contactRepo.findByUuid(contactUuid).orElseThrow(); 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 saved = contactRepo.save(current);
final var mapped = mapper.map(saved, HsOfficeContactResource.class); final var mapped = mapper.map(saved, HsOfficeContactResource.class);
return ResponseEntity.ok(mapped); return ResponseEntity.ok(mapped);
} }
@SuppressWarnings("unchecked")
final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putEmailAddresses(from(resource.getEmailAddresses()));
entity.putPhoneNumbers(from(resource.getPhoneNumbers()));
};
} }

View File

@ -1,17 +1,22 @@
package net.hostsharing.hsadminng.hs.office.contact; package net.hostsharing.hsadminng.hs.office.contact;
import io.hypersistence.utils.hibernate.type.json.JsonType;
import lombok.*; import lombok.*;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Type;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
@ -44,17 +49,45 @@ public class HsOfficeContactEntity implements Stringifyable, RbacObject {
@Version @Version
private int version; private int version;
@Column(name = "label") @Column(name = "label") // TODO.impl: rename to caption
private String label; private String label;
@Column(name = "postaladdress") @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") @Builder.Default
private String emailAddresses; // TODO.spec: check if we can really add multiple. format: ["eins@...", "zwei@..."] @Setter(AccessLevel.NONE)
@Type(JsonType.class)
@Column(name = "emailaddresses")
private Map<String, String> emailAddresses = new HashMap<>();
@Column(name = "phonenumbers", columnDefinition = "json") @Transient
private String phoneNumbers; // TODO.spec: check if we can really add multiple. format: { "office": "+49 40 12345-10", "fax": "+49 40 12345-05" } private PatchableMapWrapper<String> emailAddressesWrapper;
@Builder.Default
@Setter(AccessLevel.NONE)
@Type(JsonType.class)
@Column(name = "phonenumbers")
private Map<String, String> phoneNumbers = new HashMap<>();
@Transient
private PatchableMapWrapper<String> phoneNumbersWrapper;
public PatchableMapWrapper<String> getEmailAddresses() {
return PatchableMapWrapper.of(emailAddressesWrapper, (newWrapper) -> {emailAddressesWrapper = newWrapper; }, emailAddresses );
}
public void putEmailAddresses(Map<String, String> newEmailAddresses) {
getEmailAddresses().assign(newEmailAddresses);
}
public PatchableMapWrapper<String> getPhoneNumbers() {
return PatchableMapWrapper.of(phoneNumbersWrapper, (newWrapper) -> {phoneNumbersWrapper = newWrapper; }, phoneNumbers );
}
public void putPhoneNumbers(Map<String, String> newPhoneNumbers) {
getPhoneNumbers().assign(newPhoneNumbers);
}
@Override @Override
public String toString() { public String toString() {

View File

@ -1,14 +1,17 @@
package net.hostsharing.hsadminng.hs.office.contact; package net.hostsharing.hsadminng.hs.office.contact;
import net.hostsharing.hsadminng.mapper.EntityPatcher; import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.KeyValueMap;
import net.hostsharing.hsadminng.mapper.OptionalFromJson; import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactPatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactPatchResource;
class HsOfficeContactEntityPatch implements EntityPatcher<HsOfficeContactPatchResource> { import java.util.Optional;
class HsOfficeContactEntityPatcher implements EntityPatcher<HsOfficeContactPatchResource> {
private final HsOfficeContactEntity entity; private final HsOfficeContactEntity entity;
HsOfficeContactEntityPatch(final HsOfficeContactEntity entity) { HsOfficeContactEntityPatcher(final HsOfficeContactEntity entity) {
this.entity = entity; this.entity = entity;
} }
@ -16,7 +19,9 @@ class HsOfficeContactEntityPatch implements EntityPatcher<HsOfficeContactPatchRe
public void apply(final HsOfficeContactPatchResource resource) { public void apply(final HsOfficeContactPatchResource resource) {
OptionalFromJson.of(resource.getLabel()).ifPresent(entity::setLabel); OptionalFromJson.of(resource.getLabel()).ifPresent(entity::setLabel);
OptionalFromJson.of(resource.getPostalAddress()).ifPresent(entity::setPostalAddress); OptionalFromJson.of(resource.getPostalAddress()).ifPresent(entity::setPostalAddress);
OptionalFromJson.of(resource.getEmailAddresses()).ifPresent(entity::setEmailAddresses); Optional.ofNullable(resource.getEmailAddresses())
OptionalFromJson.of(resource.getPhoneNumbers()).ifPresent(entity::setPhoneNumbers); .ifPresent(r -> entity.getEmailAddresses().patch(KeyValueMap.from(resource.getEmailAddresses())));
Optional.ofNullable(resource.getPhoneNumbers())
.ifPresent(r -> entity.getPhoneNumbers().patch(KeyValueMap.from(resource.getPhoneNumbers())));
} }
} }

View File

@ -170,7 +170,7 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
"vatCountryCode", "vatCountryCode",
"vatBusiness", "vatBusiness",
"vatReverseCharge", "vatReverseCharge",
"defaultPrefix" /* TODO.spec: do we want that updatable? */) "defaultPrefix")
.toRole("global", ADMIN).grantPermission(INSERT) .toRole("global", ADMIN).grantPermission(INSERT)
.importRootEntityAliasProxy("debitorRel", HsOfficeRelationEntity.class, usingCase(DEBITOR), .importRootEntityAliasProxy("debitorRel", HsOfficeRelationEntity.class, usingCase(DEBITOR),

View File

@ -142,7 +142,7 @@ public class HsOfficeRelationEntity implements RbacObject, Stringifyable {
with.permission(UPDATE); with.permission(UPDATE);
}) })
.createSubRole(AGENT, (with) -> { .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, // the alternative would be to move this to the relation:ADMIN role,
// but then the partner holder person could update the partner relation itself, // but then the partner holder person could update the partner relation itself,
// see partner entity. // see partner entity.

View File

@ -5,9 +5,9 @@ import java.util.Map;
public class KeyValueMap { public class KeyValueMap {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static Map<String, Object> from(final Object obj) { public static <T> Map<String, T> from(final Object obj) {
if (obj instanceof Map<?, ?>) { if (obj == null || obj instanceof Map<?, ?>) {
return (Map<String, Object>) obj; return (Map<String, T>) obj;
} }
throw new ClassCastException("Map expected, but got: " + obj); throw new ClassCastException("Map expected, but got: " + obj);
} }

View File

@ -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 * 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. * thus JSON nullable object structures from HTTP PATCH can be represented.
*/ */
public class PatchMap extends TreeMap<String, Object> { public class PatchMap<T> extends TreeMap<String, T> {
public PatchMap(final ImmutablePair<String, Object>[] entries) { public PatchMap(final ImmutablePair<String, T>[] entries) {
stream(entries).forEach(r -> put(r.getKey(), r.getValue())); stream(entries).forEach(r -> put(r.getKey(), r.getValue()));
} }
@SafeVarargs @SafeVarargs
public static Map<String, Object> patchMap(final ImmutablePair<String, Object>... entries) { public static <T> Map<String, T> patchMap(final ImmutablePair<String, Object>... entries) {
return new PatchMap(entries); return new PatchMap(entries);
} }
@NotNull @NotNull
public static ImmutablePair<String, Object> entry(final String key, final Object value) { public static <T> ImmutablePair<String, T> entry(final String key, final T value) {
return new ImmutablePair<>(key, value); return new ImmutablePair<>(key, value);
} }
} }

View File

@ -6,31 +6,43 @@ import jakarta.validation.constraints.NotNull;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.joining;
/** This class wraps another (usually persistent) map and /** This class wraps another (usually persistent) map and
* supports applying `PatchMap` as well as a toString method with stable entry order. * supports applying `PatchMap` as well as a toString method with stable entry order.
*/ */
public class PatchableMapWrapper implements Map<String, Object> { public class PatchableMapWrapper<T> implements Map<String, T> {
private final Map<String, Object> delegate; private final Map<String, T> delegate;
public PatchableMapWrapper(final Map<String, Object> map) { private PatchableMapWrapper(final Map<String, T> map) {
delegate = map; delegate = map;
} }
public static <T> PatchableMapWrapper<T> of(final PatchableMapWrapper<T> currentWrapper, final Consumer<PatchableMapWrapper<T>> setWrapper, final Map<String, T> target) {
return ofNullable(currentWrapper).orElseGet(() -> {
final var newWrapper = new PatchableMapWrapper<T>(target);
setWrapper.accept(newWrapper);
return newWrapper;
});
}
@NotNull @NotNull
public static ImmutablePair<String, Object> entry(final String key, final Object value) { public static <E> ImmutablePair<String, E> entry(final String key, final E value) {
return new ImmutablePair<>(key, value); return new ImmutablePair<>(key, value);
} }
public void assign(final Map<String, Object> entries) { public void assign(final Map<String, T> entries) {
if (entries != null ) {
delegate.clear(); delegate.clear();
delegate.putAll(entries); delegate.putAll(entries);
} }
}
public void patch(final Map<String, Object> patch) { public void patch(final Map<String, T> patch) {
patch.forEach((key, value) -> { patch.forEach((key, value) -> {
if (value == null) { if (value == null) {
remove(key); remove(key);
@ -73,22 +85,22 @@ public class PatchableMapWrapper implements Map<String, Object> {
} }
@Override @Override
public Object get(final Object key) { public T get(final Object key) {
return delegate.get(key); return delegate.get(key);
} }
@Override @Override
public Object put(final String key, final Object value) { public T put(final String key, final T value) {
return delegate.put(key, value); return delegate.put(key, value);
} }
@Override @Override
public Object remove(final Object key) { public T remove(final Object key) {
return delegate.remove(key); return delegate.remove(key);
} }
@Override @Override
public void putAll(final Map<? extends String, ?> m) { public void putAll(final @NotNull Map<? extends String, ? extends T> m) {
delegate.putAll(m); delegate.putAll(m);
} }
@ -103,12 +115,12 @@ public class PatchableMapWrapper implements Map<String, Object> {
} }
@Override @Override
public Collection<Object> values() { public Collection<T> values() {
return delegate.values(); return delegate.values();
} }
@Override @Override
public Set<Entry<String, Object>> entrySet() { public Set<Entry<String, T>> entrySet() {
return delegate.entrySet(); return delegate.entrySet();
} }
} }

View File

@ -4,7 +4,9 @@ import net.hostsharing.hsadminng.errors.DisplayName;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
@ -62,6 +64,7 @@ public final class Stringify<B> {
final var propValues = props.stream() final var propValues = props.stream()
.map(prop -> PropertyValue.of(prop, prop.getter.apply(object))) .map(prop -> PropertyValue.of(prop, prop.getter.apply(object)))
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(PropertyValue::nonEmpty)
.map(propVal -> { .map(propVal -> {
if (propVal.rawValue instanceof Stringifyable stringifyable) { if (propVal.rawValue instanceof Stringifyable stringifyable) {
return new PropertyValue<>(propVal.prop, propVal.rawValue, stringifyable.toShortString()); return new PropertyValue<>(propVal.prop, propVal.rawValue, stringifyable.toShortString());
@ -110,5 +113,12 @@ public final class Stringify<B> {
static <B> PropertyValue<B> of(Property<B> prop, Object rawValue) { static <B> PropertyValue<B> of(Property<B> prop, Object rawValue) {
return rawValue != null ? new PropertyValue<>(prop, rawValue, rawValue.toString()) : null; 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());
}
} }
} }

View File

@ -14,9 +14,9 @@ components:
postalAddress: postalAddress:
type: string type: string
emailAddresses: emailAddresses:
type: string $ref: '#/components/schemas/HsOfficeContactEmailAddresses'
phoneNumbers: phoneNumbers:
type: string $ref: '#/components/schemas/HsOfficeContactPhoneNumbers'
HsOfficeContactInsert: HsOfficeContactInsert:
type: object type: object
@ -26,9 +26,9 @@ components:
postalAddress: postalAddress:
type: string type: string
emailAddresses: emailAddresses:
type: string $ref: '#/components/schemas/HsOfficeContactEmailAddresses'
phoneNumbers: phoneNumbers:
type: string $ref: '#/components/schemas/HsOfficeContactPhoneNumbers'
required: required:
- label - label
@ -42,8 +42,31 @@ components:
type: string type: string
nullable: true nullable: true
emailAddresses: emailAddresses:
type: string $ref: '#/components/schemas/HsOfficeContactEmailAddresses'
nullable: true
phoneNumbers: phoneNumbers:
$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 type: string
nullable: true nullable: true
phone_private:
type: string
nullable: true
phone_mobile:
type: string
nullable: true
fax:
type: string
nullable: true
additionalProperties: false

View File

@ -63,7 +63,7 @@ begin
insert insert
into RbacGrants (grantedByRoleUuid, ascendantUuid, descendantUuid, assumed) into RbacGrants (grantedByRoleUuid, ascendantUuid, descendantUuid, assumed)
values (grantedByRoleUuid, userUuid, grantedRoleUuid, doAssume); 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? -- Most powerful or latest grant wins? What about managed?
-- on conflict do nothing; -- allow granting multiple times -- on conflict do nothing; -- allow granting multiple times
end; $$; end; $$;

View File

@ -52,7 +52,7 @@ begin
if cardinality(userUuids) > 0 then if cardinality(userUuids) > 0 then
-- direct grants to users need a grantedByRole which can revoke the grant -- direct grants to users need a grantedByRole which can revoke the grant
if grantedByRole is null then 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 else
userGrantsByRoleUuid := getRoleId(grantedByRole); userGrantsByRoleUuid := getRoleId(grantedByRole);
end if; end if;

View File

@ -10,8 +10,8 @@ create table if not exists hs_office_contact
version int not null default 0, version int not null default 0,
label varchar(128) not null, label varchar(128) not null,
postalAddress text, postalAddress text,
emailAddresses text, -- TODO.feat: change to json emailAddresses jsonb not null,
phoneNumbers text -- TODO.feat: change to json phoneNumbers jsonb not null
); );
--// --//

View File

@ -12,6 +12,7 @@ create or replace procedure createHsOfficeContactTestData(contLabel varchar)
language plpgsql as $$ language plpgsql as $$
declare declare
currentTask varchar; currentTask varchar;
postalAddr varchar;
emailAddr varchar; emailAddr varchar;
begin begin
currentTask = 'creating contact test-data ' || contLabel; currentTask = 'creating contact test-data ' || contLabel;
@ -22,14 +23,17 @@ begin
perform createRbacUser(emailAddr); perform createRbacUser(emailAddr);
call defineContext(currentTask, null, emailAddr); call defineContext(currentTask, null, emailAddr);
postalAddr := E'Vorname Nachname\nStraße Hnr\nPLZ Stadt';
raise notice 'creating test contact: %', contLabel; raise notice 'creating test contact: %', contLabel;
insert insert
into hs_office_contact (label, postaladdress, emailaddresses, phonenumbers) into hs_office_contact (label, postaladdress, emailaddresses, phonenumbers)
values (contLabel, $address$ values (
Vorname Nachname contLabel,
Straße Hnr postalAddr,
PLZ Stadt ('{ "main": "' || emailAddr || '" }')::jsonb,
$address$, emailAddr, '+49 123 1234567'); ('{ "phone_office": "+49 123 1234567" }')::jsonb
);
end; $$; end; $$;
--// --//

View File

@ -11,7 +11,7 @@ create table hs_office_debitor
debitorNumberSuffix char(2) not null check (debitorNumberSuffix::text ~ '^[0-9][0-9]$'), debitorNumberSuffix char(2) not null check (debitorNumberSuffix::text ~ '^[0-9][0-9]$'),
debitorRelUuid uuid not null references hs_office_relation(uuid), debitorRelUuid uuid not null references hs_office_relation(uuid),
billable boolean not null default true, billable boolean not null default true,
vatId varchar(24), -- TODO.spec: here or in person? vatId varchar(24),
vatCountryCode varchar(2), vatCountryCode varchar(2),
vatBusiness boolean not null, vatBusiness boolean not null,
vatReverseCharge boolean not null, vatReverseCharge boolean not null,

View File

@ -19,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.hsadminng.rbac.test.IsValidUuidMatcher.isUuidValid; import static net.hostsharing.hsadminng.rbac.test.IsValidUuidMatcher.isUuidValid;
@ -103,7 +104,9 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.body(""" .body("""
{ {
"label": "Temp Contact", "label": "Temp Contact",
"emailAddresses": "test@example.org" "emailAddresses": {
"main": "test@example.org"
}
} }
""") """)
.port(port) .port(port)
@ -114,7 +117,7 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body("uuid", isUuidValid()) .body("uuid", isUuidValid())
.body("label", is("Temp Contact")) .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")) .header("Location", startsWith("http://localhost"))
.extract().header("Location"); // @formatter:on .extract().header("Location"); // @formatter:on
@ -181,8 +184,12 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.body("", lenientlyEquals(""" .body("", lenientlyEquals("""
{ {
"label": "first contact", "label": "first contact",
"emailAddresses": "contact-admin@firstcontact.example.com", "emailAddresses": {
"phoneNumbers": "+49 123 1234567" "main": "contact-admin@firstcontact.example.com"
},
"phoneNumbers": {
"phone_office": "+49 123 1234567"
}
} }
""")); // @formatter:on """)); // @formatter:on
} }
@ -204,9 +211,13 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.body(""" .body("""
{ {
"label": "Temp patched contact", "label": "Temp patched contact",
"emailAddresses": "patched@example.org", "emailAddresses": {
"main": "patched@example.org"
},
"postalAddress": "Patched Address", "postalAddress": "Patched Address",
"phoneNumbers": "+01 100 123456" "phoneNumbers": {
"phone_office": "+01 100 123456"
}
} }
""") """)
.port(port) .port(port)
@ -217,9 +228,9 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body("uuid", isUuidValid()) .body("uuid", isUuidValid())
.body("label", is("Temp patched contact")) .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("postalAddress", is("Patched Address"))
.body("phoneNumbers", is("+01 100 123456")); .body("phoneNumbers", is(Map.of("phone_office", "+01 100 123456")));
// @formatter:on // @formatter:on
// finally, the contact is actually updated // finally, the contact is actually updated
@ -227,9 +238,9 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
assertThat(contactRepo.findByUuid(givenContact.getUuid())).isPresent().get() assertThat(contactRepo.findByUuid(givenContact.getUuid())).isPresent().get()
.matches(person -> { .matches(person -> {
assertThat(person.getLabel()).isEqualTo("Temp patched contact"); 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.getPostalAddress()).isEqualTo("Patched Address");
assertThat(person.getPhoneNumbers()).isEqualTo("+01 100 123456"); assertThat(person.getPhoneNumbers()).containsExactlyEntriesOf(Map.of("phone_office", "+01 100 123456"));
return true; return true;
}); });
} }
@ -246,8 +257,12 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(""" .body("""
{ {
"emailAddresses": "patched@example.org", "emailAddresses": {
"phoneNumbers": "+01 100 123456" "main": "patched@example.org"
},
"phoneNumbers": {
"phone_office": "+01 100 123456"
}
} }
""") """)
.port(port) .port(port)
@ -258,18 +273,18 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body("uuid", isUuidValid()) .body("uuid", isUuidValid())
.body("label", is(givenContact.getLabel())) .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("postalAddress", is(givenContact.getPostalAddress()))
.body("phoneNumbers", is("+01 100 123456")); .body("phoneNumbers", is(Map.of("phone_office", "+01 100 123456")));
// @formatter:on // @formatter:on
// finally, the contact is actually updated // finally, the contact is actually updated
assertThat(contactRepo.findByUuid(givenContact.getUuid())).isPresent().get() assertThat(contactRepo.findByUuid(givenContact.getUuid())).isPresent().get()
.matches(person -> { .matches(person -> {
assertThat(person.getLabel()).isEqualTo(givenContact.getLabel()); 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.getPostalAddress()).isEqualTo(givenContact.getPostalAddress());
assertThat(person.getPhoneNumbers()).isEqualTo("+01 100 123456"); assertThat(person.getPhoneNumbers()).containsExactlyEntriesOf(Map.of("phone_office", "+01 100 123456"));
return true; return true;
}); });
} }
@ -340,9 +355,9 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
final var newContact = HsOfficeContactEntity.builder() final var newContact = HsOfficeContactEntity.builder()
.uuid(UUID.randomUUID()) .uuid(UUID.randomUUID())
.label("Temp from " + Context.getCallerMethodNameFromStackFrame(1) ) .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)) .postalAddress("Postal Address " + RandomStringUtils.randomAlphabetic(10))
.phoneNumbers("+01 200 " + RandomStringUtils.randomNumeric(8)) .phoneNumbers(Map.of("phone_office", "+01 200 " + RandomStringUtils.randomNumeric(8)))
.build(); .build();
return contactRepo.save(newContact); return contactRepo.save(newContact);

View File

@ -4,9 +4,12 @@ import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactPatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactPatchResource;
import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Stream; 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; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
@TestInstance(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 UUID INITIAL_CONTACT_UUID = UUID.randomUUID();
private static final Map<String, String> PATCH_EMAIL_ADDRESSES = patchMap(
entry("main", "patched@example.com"),
entry("paul", null),
entry("suse", "suse@example.com")
);
private static final Map<String, String> PATCHED_EMAIL_ADDRESSES = patchMap(
entry("main", "patched@example.com"),
entry("suse", "suse@example.com"),
entry("mila", "mila@example.com")
);
private static final Map<String, String> PATCH_PHONE_NUMBERS = patchMap(
entry("phone_mobile", null),
entry("phone_private", "+49 40 987654321"),
entry("fax", "+49 40 12345-99")
);
private static final Map<String, String> PATCHED_PHONE_NUMBERS = patchMap(
entry("phone_office", "+49 40 12345-00"),
entry("phone_private", "+49 40 987654321"),
entry("fax", "+49 40 12345-99")
);
@Override @Override
protected HsOfficeContactEntity newInitialEntity() { protected HsOfficeContactEntity newInitialEntity() {
final var entity = new HsOfficeContactEntity(); final var entity = new HsOfficeContactEntity();
entity.setUuid(INITIAL_CONTACT_UUID); entity.setUuid(INITIAL_CONTACT_UUID);
entity.setLabel("initial label"); entity.setLabel("initial label");
entity.setEmailAddresses("initial@example.org"); entity.putEmailAddresses(Map.ofEntries(
entity.setPhoneNumbers("initial postal address"); entry("main", "initial@example.org"),
entity.setPostalAddress("+01 100 123456789"); 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; return entity;
} }
@ -34,8 +64,8 @@ class HsOfficeContactEntityPatcherUnitTest extends PatchUnitTestBase<
} }
@Override @Override
protected HsOfficeContactEntityPatch createPatcher(final HsOfficeContactEntity entity) { protected HsOfficeContactEntityPatcher createPatcher(final HsOfficeContactEntity entity) {
return new HsOfficeContactEntityPatch(entity); return new HsOfficeContactEntityPatcher(entity);
} }
@Override @Override
@ -46,16 +76,20 @@ class HsOfficeContactEntityPatcherUnitTest extends PatchUnitTestBase<
HsOfficeContactPatchResource::setLabel, HsOfficeContactPatchResource::setLabel,
"patched label", "patched label",
HsOfficeContactEntity::setLabel), HsOfficeContactEntity::setLabel),
new JsonNullableProperty<>( new SimpleProperty<>(
"emailAddresses", "resources",
HsOfficeContactPatchResource::setEmailAddresses, HsOfficeContactPatchResource::setEmailAddresses,
"patched trade name", PATCH_EMAIL_ADDRESSES,
HsOfficeContactEntity::setEmailAddresses), HsOfficeContactEntity::putEmailAddresses,
new JsonNullableProperty<>( PATCHED_EMAIL_ADDRESSES)
"phoneNumbers", .notNullable(),
new SimpleProperty<>(
"resources",
HsOfficeContactPatchResource::setPhoneNumbers, HsOfficeContactPatchResource::setPhoneNumbers,
"patched family name", PATCH_PHONE_NUMBERS,
HsOfficeContactEntity::setPhoneNumbers), HsOfficeContactEntity::putPhoneNumbers,
PATCHED_PHONE_NUMBERS)
.notNullable(),
new JsonNullableProperty<>( new JsonNullableProperty<>(
"patched given name", "patched given name",
HsOfficeContactPatchResource::setPostalAddress, HsOfficeContactPatchResource::setPostalAddress,

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.hs.office.contact; package net.hostsharing.hsadminng.hs.office.contact;
import java.util.Map;
public class TestHsOfficeContact { public class TestHsOfficeContact {
@ -9,7 +10,7 @@ public class TestHsOfficeContact {
return HsOfficeContactEntity.builder() return HsOfficeContactEntity.builder()
.label(label) .label(label)
.postalAddress("address of " + label) .postalAddress("address of " + label)
.emailAddresses(emailAddr) .emailAddresses(Map.of("main", emailAddr))
.build(); .build();
} }
} }

View File

@ -107,8 +107,8 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
"mark": null, "mark": null,
"contact": { "contact": {
"label": "first contact", "label": "first contact",
"emailAddresses": "contact-admin@firstcontact.example.com", "emailAddresses": { "main": "contact-admin@firstcontact.example.com" },
"phoneNumbers": "+49 123 1234567" "phoneNumbers": { "phone_office": "+49 123 1234567" }
} }
}, },
"debitorNumber": 1000111, "debitorNumber": 1000111,
@ -132,8 +132,8 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
"mark": null, "mark": null,
"contact": { "contact": {
"label": "first contact", "label": "first contact",
"emailAddresses": "contact-admin@firstcontact.example.com", "emailAddresses": { "main": "contact-admin@firstcontact.example.com" },
"phoneNumbers": "+49 123 1234567" "phoneNumbers": { "phone_office": "+49 123 1234567" }
} }
}, },
"details": { "details": {
@ -162,7 +162,9 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
"anchor": {"tradeName": "Second e.K."}, "anchor": {"tradeName": "Second e.K."},
"holder": {"tradeName": "Second e.K."}, "holder": {"tradeName": "Second e.K."},
"type": "DEBITOR", "type": "DEBITOR",
"contact": {"emailAddresses": "contact-admin@secondcontact.example.com"} "contact": {
"emailAddresses": { "main": "contact-admin@secondcontact.example.com" }
}
}, },
"debitorNumber": 1000212, "debitorNumber": 1000212,
"debitorNumberSuffix": 12, "debitorNumberSuffix": 12,
@ -172,7 +174,9 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
"anchor": {"tradeName": "Hostsharing eG"}, "anchor": {"tradeName": "Hostsharing eG"},
"holder": {"tradeName": "Second e.K."}, "holder": {"tradeName": "Second e.K."},
"type": "PARTNER", "type": "PARTNER",
"contact": {"emailAddresses": "contact-admin@secondcontact.example.com"} "contact": {
"emailAddresses": { "main": "contact-admin@secondcontact.example.com" }
}
}, },
"details": { "details": {
"registrationOffice": "Hamburg", "registrationOffice": "Hamburg",
@ -192,7 +196,9 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
"anchor": {"tradeName": "Third OHG"}, "anchor": {"tradeName": "Third OHG"},
"holder": {"tradeName": "Third OHG"}, "holder": {"tradeName": "Third OHG"},
"type": "DEBITOR", "type": "DEBITOR",
"contact": {"emailAddresses": "contact-admin@thirdcontact.example.com"} "contact": {
"emailAddresses": { "main": "contact-admin@thirdcontact.example.com" }
}
}, },
"debitorNumber": 1000313, "debitorNumber": 1000313,
"debitorNumberSuffix": 13, "debitorNumberSuffix": 13,
@ -202,7 +208,9 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
"anchor": {"tradeName": "Hostsharing eG"}, "anchor": {"tradeName": "Hostsharing eG"},
"holder": {"tradeName": "Third OHG"}, "holder": {"tradeName": "Third OHG"},
"type": "PARTNER", "type": "PARTNER",
"contact": {"emailAddresses": "contact-admin@thirdcontact.example.com"} "contact": {
"emailAddresses": { "main": "contact-admin@thirdcontact.example.com" }
}
}, },
"details": { "details": {
"registrationOffice": "Hamburg", "registrationOffice": "Hamburg",
@ -223,7 +231,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
} }
@Test @Test
void globalAdmin_withoutAssumedRoles_canFindDebitorDebitorByDebitorNumber() throws JSONException { void globalAdmin_withoutAssumedRoles_canFindDebitorDebitorByDebitorNumber() {
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
@ -456,9 +464,9 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
"type": "DEBITOR", "type": "DEBITOR",
"contact": { "contact": {
"label": "first contact", "label": "first contact",
"postalAddress": "\\nVorname Nachname\\nStraße Hnr\\nPLZ Stadt\\n", "postalAddress": "Vorname Nachname\\nStraße Hnr\\nPLZ Stadt",
"emailAddresses": "contact-admin@firstcontact.example.com", "emailAddresses": { "main": "contact-admin@firstcontact.example.com" },
"phoneNumbers": "+49 123 1234567" "phoneNumbers": { "phone_office": "+49 123 1234567" }
} }
}, },
"debitorNumber": 1000111, "debitorNumber": 1000111,
@ -472,9 +480,9 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
"mark": null, "mark": null,
"contact": { "contact": {
"label": "first contact", "label": "first contact",
"postalAddress": "\\nVorname Nachname\\nStraße Hnr\\nPLZ Stadt\\n", "postalAddress": "Vorname Nachname\\nStraße Hnr\\nPLZ Stadt",
"emailAddresses": "contact-admin@firstcontact.example.com", "emailAddresses": { "main": "contact-admin@firstcontact.example.com" },
"phoneNumbers": "+49 123 1234567" "phoneNumbers": { "phone_office": "+49 123 1234567" }
} }
}, },
"details": { "details": {

View File

@ -240,29 +240,29 @@ public class ImportOfficeData extends ContextBasedTest {
"""); """);
assertThat(toFormattedString(contacts)).isEqualToIgnoringWhitespace(""" assertThat(toFormattedString(contacts)).isEqualToIgnoringWhitespace("""
{ {
1101=contact(label='Herr Michael Mellies ', emailAddresses='mih@example.org'), 1101=contact(label='Herr Michael Mellies ', emailAddresses='{ main: mih@example.org }'),
1200=contact(label='JM e.K.', emailAddresses='jm-ex-partner@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='jm-billing@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='am-operation@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='pm-partner@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='tm-vip@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='ps@example.com'), 1301=contact(label='Petra Schmidt , Test PS', emailAddresses='{ main: ps@example.com }'),
1401=contact(label='Frau Frauke Fanninga ', emailAddresses='ff@example.org'), 1401=contact(label='Frau Frauke Fanninga ', emailAddresses='{ main: ff@example.org }'),
1501=contact(label='Frau Cecilia Camus ', emailAddresses='cc@example.org') 1501=contact(label='Frau Cecilia Camus ', emailAddresses='{ main: cc@example.org }')
} }
"""); """);
assertThat(toFormattedString(persons)).isEqualToIgnoringWhitespace(""" assertThat(toFormattedString(persons)).isEqualToIgnoringWhitespace("""
{ {
1=person(personType='LP', tradeName='Hostsharing eG'), 1=person(personType='LP', tradeName='Hostsharing eG'),
1101=person(personType='NP', tradeName='', familyName='Mellies', givenName='Michael'), 1101=person(personType='NP', familyName='Mellies', givenName='Michael'),
1200=person(personType='LP', tradeName='JM e.K.', familyName='', givenName=''), 1200=person(personType='LP', tradeName='JM e.K.'),
1201=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Billing', givenName='Jenny'), 1201=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Billing', givenName='Jenny'),
1202=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Operation', givenName='Andrew'), 1202=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Operation', givenName='Andrew'),
1203=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Contract', givenName='Philip'), 1203=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Contract', givenName='Philip'),
1204=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-VIP', givenName='Tammy'), 1204=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-VIP', givenName='Tammy'),
1301=person(personType='??', tradeName='Test PS', familyName='Schmidt', givenName='Petra'), 1301=person(personType='??', tradeName='Test PS', familyName='Schmidt', givenName='Petra'),
1401=person(personType='NP', tradeName='', familyName='Fanninga', givenName='Frauke'), 1401=person(personType='NP', familyName='Fanninga', givenName='Frauke'),
1501=person(personType='NP', tradeName='', familyName='Camus', givenName='Cecilia') 1501=person(personType='NP', familyName='Camus', givenName='Cecilia')
} }
"""); """);
assertThat(toFormattedString(debitors)).isEqualToIgnoringWhitespace(""" assertThat(toFormattedString(debitors)).isEqualToIgnoringWhitespace("""
@ -363,10 +363,10 @@ public class ImportOfficeData extends ContextBasedTest {
assertThat(toFormattedString(coopShares)).isEqualToIgnoringWhitespace(""" assertThat(toFormattedString(coopShares)).isEqualToIgnoringWhitespace("""
{ {
33443=CoopShareTransaction(M-1001700: 2000-12-06, SUBSCRIPTION, 20, legacy data import, initial share subscription), 33443=CoopShareTransaction(M-1001700: 2000-12-06, SUBSCRIPTION, 20, 1001700, initial share subscription),
33451=CoopShareTransaction(M-1002000: 2000-12-06, SUBSCRIPTION, 2, legacy data import, 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, legacy data import, increase), 33701=CoopShareTransaction(M-1001700: 2005-01-10, SUBSCRIPTION, 40, 1001700, increase),
33810=CoopShareTransaction(M-1002000: 2016-12-31, CANCELLATION, 22, legacy data import, membership ended) 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(""" assertThat(toFormattedString(coopAssets)).isEqualToIgnoringWhitespace("""
{ {
30000=CoopAssetsTransaction(M-1001700: 2000-12-06, DEPOSIT, 1280.00, legacy data import, for subscription A), 30000=CoopAssetsTransaction(M-1001700: 2000-12-06, DEPOSIT, 1280.00, 1001700, for subscription A),
31000=CoopAssetsTransaction(M-1002000: 2000-12-06, DEPOSIT, 128.00, legacy data import, for subscription B), 31000=CoopAssetsTransaction(M-1002000: 2000-12-06, DEPOSIT, 128.00, 1002000, for subscription B),
32000=CoopAssetsTransaction(M-1001700: 2005-01-10, DEPOSIT, 2560.00, legacy data import, for subscription C), 32000=CoopAssetsTransaction(M-1001700: 2005-01-10, DEPOSIT, 2560.00, 1001700, for subscription C),
33001=CoopAssetsTransaction(M-1001700: 2005-01-10, TRANSFER, -512.00, legacy data import, for transfer to 10), 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, legacy data import, for transfer from 7), 33002=CoopAssetsTransaction(M-1002000: 2005-01-10, ADOPTION, 512.00, 1002000, for transfer from 7),
34001=CoopAssetsTransaction(M-1002000: 2016-12-31, CLEARING, -8.00, legacy data import, for cancellation D), 34001=CoopAssetsTransaction(M-1002000: 2016-12-31, CLEARING, -8.00, 1002000, for cancellation D),
34002=CoopAssetsTransaction(M-1002000: 2016-12-31, DISBURSAL, -100.00, legacy data import, 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, legacy data import, 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, legacy data import, for subscription E), 35001=CoopAssetsTransaction(M-1909000: 2024-01-15, DEPOSIT, 128.00, 1909000, for subscription E),
35002=CoopAssetsTransaction(M-1909000: 2024-01-20, ADJUSTMENT, -128.00, legacy data import, chargeback for subscription E, M-1909000:DEP:+128.00) 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")) .shareCount(rec.getInteger("quantity"))
.comment( rec.getString("comment")) .comment( rec.getString("comment"))
.reference("legacy data import") // TODO.spec: or use value from comment column? .reference(member.getMemberNumber().toString())
.build(); .build();
if (shareTransaction.getTransactionType() == HsOfficeCoopSharesTransactionType.ADJUSTMENT) { if (shareTransaction.getTransactionType() == HsOfficeCoopSharesTransactionType.ADJUSTMENT) {
@ -867,7 +867,7 @@ public class ImportOfficeData extends ContextBasedTest {
.transactionType(assetTypeMapping.get(rec.getString("action"))) .transactionType(assetTypeMapping.get(rec.getString("action")))
.assetValue(rec.getBigDecimal("amount")) .assetValue(rec.getBigDecimal("amount"))
.comment(rec.getString("comment")) .comment(rec.getString("comment"))
.reference("legacy data import") // TODO.spec: or use value from comment column? .reference(member.getMemberNumber().toString())
.build(); .build();
if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.ADJUSTMENT) { if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.ADJUSTMENT) {
@ -1092,9 +1092,9 @@ public class ImportOfficeData extends ContextBasedTest {
contactRecord.getString("first_name"), contactRecord.getString("first_name"),
contactRecord.getString("last_name"), contactRecord.getString("last_name"),
contactRecord.getString("firma"))); contactRecord.getString("firma")));
contact.setEmailAddresses(contactRecord.getString("email")); contact.putEmailAddresses( Map.of("main", contactRecord.getString("email")));
contact.setPostalAddress(toAddress(contactRecord)); contact.setPostalAddress(toAddress(contactRecord));
contact.setPhoneNumbers(toPhoneNumbers(contactRecord)); contact.putPhoneNumbers(toPhoneNumbers(contactRecord));
contacts.put(contactRecord.getInteger("contact_id"), contact); contacts.put(contactRecord.getInteger("contact_id"), contact);
return contact; return contact;
@ -1120,17 +1120,17 @@ public class ImportOfficeData extends ContextBasedTest {
return record; return record;
} }
private String toPhoneNumbers(final Record rec) { private Map<String, String> toPhoneNumbers(final Record rec) {
final var result = new StringBuilder("{\n"); final var phoneNumbers = new LinkedHashMap<String, String>();
if (isNotBlank(rec.getString("phone_private"))) 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"))) 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"))) 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"))) if (isNotBlank(rec.getString("fax")))
result.append(" \"fax\": " + "\"" + rec.getString("fax") + "\",\n"); phoneNumbers.put("fax", rec.getString("fax"));
return (result + "}").replace("\",\n}", "\"\n}"); return phoneNumbers;
} }
private String toAddress(final Record rec) { private String toAddress(final Record rec) {

View File

@ -14,6 +14,19 @@ class TestPackageEntityUnitTest {
%%{init:{'flowchart':{'htmlLabels':false}}}%% %%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB 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**`"] subgraph package["`**package**`"]
direction TB direction TB
style package fill:#dd4901,stroke:#274d6e,stroke-width:8px style package fill:#dd4901,stroke:#274d6e,stroke-width:8px
@ -36,19 +49,6 @@ class TestPackageEntityUnitTest {
end end
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 %% granting roles to roles
role:global:ADMIN -.->|XX| role:customer:OWNER role:global:ADMIN -.->|XX| role:customer:OWNER
role:customer:OWNER -.-> role:customer:ADMIN role:customer:OWNER -.-> role:customer:ADMIN