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 0a041763..dc379e22 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 @@ -13,12 +13,11 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import jakarta.annotation.PostConstruct; import java.util.List; import java.util.UUID; -import java.util.function.BiConsumer; import static net.hostsharing.hsadminng.errors.Validate.validate; -import static net.hostsharing.hsadminng.mapper.KeyValueMap.from; @RestController @@ -30,9 +29,18 @@ public class HsOfficeContactController implements HsOfficeContactsApi { @Autowired private StrictMapper mapper; + @Autowired + private HsOfficeContactFromResourceConverter contactFromResourceConverter; + @Autowired private HsOfficeContactRbacRepository contactRepo; + @PostConstruct + public void init() { + // HOWTO: add a ModelMapper converter for a generic entity class to a ModelMapper to be used in a certain context + mapper.addConverter(contactFromResourceConverter, HsOfficeContactInsertResource.class, HsOfficeContactRbacEntity.class); + } + @Override @Transactional(readOnly = true) @Timed("app.office.contacts.api.getListOfContacts") @@ -62,7 +70,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi { context.define(currentSubject, assumedRoles); - final var entityToSave = mapper.map(body, HsOfficeContactRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); + final var entityToSave = mapper.map(body, HsOfficeContactRbacEntity.class); final var saved = contactRepo.save(entityToSave); @@ -128,11 +136,4 @@ public class HsOfficeContactController implements HsOfficeContactsApi { final var mapped = mapper.map(saved, HsOfficeContactResource.class); return ResponseEntity.ok(mapped); } - - @SuppressWarnings("unchecked") - final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { - entity.putPostalAddress(from(resource.getPostalAddress())); - entity.putEmailAddresses(from(resource.getEmailAddresses())); - entity.putPhoneNumbers(from(resource.getPhoneNumbers())); - }; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactFromResourceConverter.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactFromResourceConverter.java new file mode 100644 index 00000000..6798ac15 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactFromResourceConverter.java @@ -0,0 +1,32 @@ +package net.hostsharing.hsadminng.hs.office.contact; + +import lombok.SneakyThrows; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource; +import net.hostsharing.hsadminng.mapper.StrictMapper; +import org.modelmapper.Converter; +import org.modelmapper.spi.MappingContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import static net.hostsharing.hsadminng.mapper.KeyValueMap.from; + +// HOWTO: implement a ModelMapper converter which converts from a (JSON) resource instance to a generic entity instance (RBAC vs. REAL) +@Component +public class HsOfficeContactFromResourceConverter + implements Converter { + + @Autowired + private StrictMapper mapper; + + @Override + @SneakyThrows + public E convert(final MappingContext context) { + final var resource = context.getSource(); + final var entity = context.getDestinationType().getDeclaredConstructor().newInstance(); + entity.setCaption(resource.getCaption()); + entity.putPostalAddress(from(resource.getPostalAddress())); + entity.putEmailAddresses(from(resource.getEmailAddresses())); + entity.putPhoneNumbers(from(resource.getPhoneNumbers())); + return entity; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index 2c519849..e73b108a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -3,8 +3,10 @@ package net.hostsharing.hsadminng.hs.office.partner; import io.micrometer.core.annotation.Timed; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.errors.ReferenceNotFoundException; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePartnersApi; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource; @@ -22,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import jakarta.annotation.PostConstruct; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import java.util.List; @@ -41,6 +44,9 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { @Autowired private StrictMapper mapper; + @Autowired + private HsOfficeContactFromResourceConverter contactFromResourceConverter; + @Autowired private HsOfficePartnerRbacRepository rbacPartnerRepo; @@ -50,6 +56,11 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { @PersistenceContext private EntityManager em; + @PostConstruct + public void init() { + mapper.addConverter(contactFromResourceConverter, HsOfficeContactInsertResource.class, HsOfficeContactRealEntity.class); + } + @Override @Transactional(readOnly = true) @Timed("app.office.partners.api.getListOfPartners") @@ -185,7 +196,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { private void optionallyUpdateRelatedRelations(final HsOfficePartnerRbacEntity saved, final HsOfficePersonRealEntity previousPartnerPerson) { final var partnerPersonHasChanged = !saved.getPartnerRel().getHolder().getUuid().equals(previousPartnerPerson.getUuid()); if (partnerPersonHasChanged) { - final var count = em.createNativeQuery(""" + em.createNativeQuery(""" UPDATE hs_office.relation SET holderUuid = :newPartnerPersonUuid WHERE type = 'DEBITOR' AND holderUuid = :oldPartnerPersonUuid AND anchorUuid = :oldPartnerPersonUuid @@ -193,7 +204,6 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { .setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid()) .setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid()) .executeUpdate(); - System.out.println(count); // FIXME: remove em.createNativeQuery(""" UPDATE hs_office.relation SET anchorUuid = :newPartnerPersonUuid diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java index b410bcf2..efeae9c5 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java @@ -66,6 +66,9 @@ class HsOfficeRelationEntityPatcherUnitTest extends PatchUnitTestBase< { setCaption("Patched-Contact-Caption"); + setEmailAddresses(Map.ofEntries( + Map.entry("main", "patched@exampl.org") + )); } }; private static HsOfficeContactRealEntity PATCHED_CONTACT = HsOfficeContactRealEntity.builder() diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java index 100bfe75..1db638df 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java @@ -101,7 +101,10 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase httpGet("/api/hs/office/partners/%{partnerNumber}") .expecting(OK).expecting(JSON).expectObject(), path("partnerRel.holder.tradeName").contains( - "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}") + "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"), + path("partnerRel.contact.postalAddress").lenientlyContainsJson("§{communityOfHeirsPostalAddress}"), + path("partnerRel.contact.phoneNumbers.office").contains("%{communityOfHeirsOfficePhoneNumber}"), + path("partnerRel.contact.emailAddresses.main").contains("%{communityOfHeirsEmailAddress}") ); verify( @@ -116,14 +119,16 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase httpGet( - "/api/hs/office/relations?relationType=REPRESENTATIVE&personUuid=%{Person: %{representativeGivenName} %{representativeFamilyName}}") + "/api/hs/office/relations?relationType=REPRESENTATIVE&personUuid=%{Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}") .expecting(OK).expecting(JSON).expectArrayElements(1), path("[0].anchor.tradeName").contains( "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"), - path("[0].holder.familyName").contains("%{representativeFamilyName}") + path("[0].holder.familyName").contains("%{representativeFamilyName}"), + path("[0].contact.postalAddress").lenientlyContainsJson("§{communityOfHeirsPostalAddress}"), + path("[0].contact.phoneNumbers.office").contains("%{communityOfHeirsOfficePhoneNumber}"), + path("[0].contact.emailAddresses.main").contains("%{communityOfHeirsEmailAddress}") ); - verify( "Verify the Debitor-Relation", () -> httpGet( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java index 4a51932e..04ec0fe9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java @@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.scenarios.UseCase.HttpResponse; import java.util.function.Consumer; import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; +import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals; import static org.junit.jupiter.api.Assertions.fail; public class PathAssertion { @@ -27,6 +28,18 @@ public class PathAssertion { }; } + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Consumer lenientlyContainsJson(final String resolvableValue) { + return response -> { + try { + lenientlyEquals(ScenarioTest.resolve(resolvableValue, DROP_COMMENTS)).matches(response.getFromBody(path)) ; + } catch (final AssertionError e) { + // without this, the error message is often lacking important context + fail(e.getMessage() + " in `path(\"" + path + "\").contains(\"" + resolvableValue + "\")`" ); + } + }; + } + public Consumer doesNotExist() { return response -> { try { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java index d28adb26..eb82d676 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java @@ -32,6 +32,12 @@ public class TemplateResolver { return jsonQuoted(value); } }, + JSON_OBJECT('§'){ + @Override + String convert(final Object value, final Resolver resolver) { + return jsonObject(value); + } + }, URI_ENCODED('&'){ @Override String convert(final Object value, final Resolver resolver) { @@ -213,4 +219,12 @@ public class TemplateResolver { default -> "\"" + value + "\""; }; } + + private static String jsonObject(final Object value) { + return switch (value) { + case null -> null; + case String string -> "{" + string.replace("\n", " ") + "}"; + default -> throw new IllegalArgumentException("can not format " + value.getClass() + " (" + value + ") as JSON object"); + }; + } }