update dependend relations when updating partner person (#162)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/162 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
		| @@ -13,15 +13,13 @@ 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 | ||||
|  | ||||
| public class HsOfficeContactController implements HsOfficeContactsApi { | ||||
|  | ||||
|     @Autowired | ||||
| @@ -30,9 +28,20 @@ public class HsOfficeContactController implements HsOfficeContactsApi { | ||||
|     @Autowired | ||||
|     private StrictMapper mapper; | ||||
|  | ||||
|     @Autowired | ||||
|     private HsOfficeContactFromResourceConverter<HsOfficeContactRbacEntity> 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 | ||||
|         //  This @PostConstruct could be implemented in the converter, but only without generics. | ||||
|         //  But this converter is for HsOfficeContactRbacEntity and HsOfficeContactRealEntity. | ||||
|         mapper.addConverter(contactFromResourceConverter, HsOfficeContactInsertResource.class, HsOfficeContactRbacEntity.class); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     @Transactional(readOnly = true) | ||||
|     @Timed("app.office.contacts.api.getListOfContacts") | ||||
| @@ -62,7 +71,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 +137,4 @@ public class HsOfficeContactController implements HsOfficeContactsApi { | ||||
|         final var mapped = mapper.map(saved, HsOfficeContactResource.class); | ||||
|         return ResponseEntity.ok(mapped); | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unchecked") | ||||
|     final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { | ||||
|         entity.putPostalAddress(from(resource.getPostalAddress())); | ||||
|         entity.putEmailAddresses(from(resource.getEmailAddresses())); | ||||
|         entity.putPhoneNumbers(from(resource.getPhoneNumbers())); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,27 @@ | ||||
| package net.hostsharing.hsadminng.hs.office.contact; | ||||
|  | ||||
| import lombok.SneakyThrows; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource; | ||||
| import org.modelmapper.Converter; | ||||
| import org.modelmapper.spi.MappingContext; | ||||
| 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<E extends HsOfficeContact> | ||||
|         implements Converter<HsOfficeContactInsertResource, E> { | ||||
|  | ||||
|     @Override | ||||
|     @SneakyThrows | ||||
|     public E convert(final MappingContext<HsOfficeContactInsertResource, E> 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; | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
| @@ -42,14 +45,22 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { | ||||
|     private StrictMapper mapper; | ||||
|  | ||||
|     @Autowired | ||||
|     private HsOfficePartnerRbacRepository partnerRepo; | ||||
|     private HsOfficeContactFromResourceConverter<HsOfficeContactRealEntity> contactFromResourceConverter; | ||||
|  | ||||
|     @Autowired | ||||
|     private HsOfficeRelationRealRepository relationRepo; | ||||
|     private HsOfficePartnerRbacRepository rbacPartnerRepo; | ||||
|  | ||||
|     @Autowired | ||||
|     private HsOfficeRelationRealRepository realRelationRepo; | ||||
|  | ||||
|     @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") | ||||
| @@ -59,7 +70,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { | ||||
|             final String name) { | ||||
|         context.define(currentSubject, assumedRoles); | ||||
|  | ||||
|         final var entities = partnerRepo.findPartnerByOptionalNameLike(name); | ||||
|         final var entities = rbacPartnerRepo.findPartnerByOptionalNameLike(name); | ||||
|  | ||||
|         final var resources = mapper.mapList(entities, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); | ||||
|         return ResponseEntity.ok(resources); | ||||
| @@ -77,7 +88,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { | ||||
|  | ||||
|         final var entityToSave = createPartnerEntity(body); | ||||
|  | ||||
|         final var saved = partnerRepo.save(entityToSave); | ||||
|         final var saved = rbacPartnerRepo.save(entityToSave); | ||||
|  | ||||
|         final var uri = | ||||
|                 MvcUriComponentsBuilder.fromController(getClass()) | ||||
| @@ -98,7 +109,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { | ||||
|  | ||||
|         context.define(currentSubject, assumedRoles); | ||||
|  | ||||
|         final var result = partnerRepo.findByUuid(partnerUuid); | ||||
|         final var result = rbacPartnerRepo.findByUuid(partnerUuid); | ||||
|         if (result.isEmpty()) { | ||||
|             return ResponseEntity.notFound().build(); | ||||
|         } | ||||
| @@ -116,7 +127,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { | ||||
|  | ||||
|         context.define(currentSubject, assumedRoles); | ||||
|  | ||||
|         final var result = partnerRepo.findPartnerByPartnerNumber(partnerNumber); | ||||
|         final var result = rbacPartnerRepo.findPartnerByPartnerNumber(partnerNumber); | ||||
|         if (result.isEmpty()) { | ||||
|             return ResponseEntity.notFound().build(); | ||||
|         } | ||||
| @@ -133,12 +144,12 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { | ||||
|             final UUID partnerUuid) { | ||||
|         context.define(currentSubject, assumedRoles); | ||||
|  | ||||
|         final var partnerToDelete = partnerRepo.findByUuid(partnerUuid); | ||||
|         final var partnerToDelete = rbacPartnerRepo.findByUuid(partnerUuid); | ||||
|         if (partnerToDelete.isEmpty()) { | ||||
|             return ResponseEntity.notFound().build(); | ||||
|         } | ||||
|  | ||||
|         if (partnerRepo.deleteByUuid(partnerUuid) != 1) { | ||||
|         if (rbacPartnerRepo.deleteByUuid(partnerUuid) != 1) { | ||||
|             return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); | ||||
|         } | ||||
|  | ||||
| @@ -156,22 +167,55 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { | ||||
|  | ||||
|         context.define(currentSubject, assumedRoles); | ||||
|  | ||||
|         final var current = partnerRepo.findByUuid(partnerUuid).orElseThrow(); | ||||
|         final var previousPartnerRel = current.getPartnerRel(); | ||||
|         final var current = rbacPartnerRepo.findByUuid(partnerUuid).orElseThrow(); | ||||
|         final var previousPartnerPerson = current.getPartnerRel().getHolder(); | ||||
|  | ||||
|         new HsOfficePartnerEntityPatcher(em, current).apply(body); | ||||
|         new HsOfficePartnerEntityPatcher(mapper, em, current).apply(body); | ||||
|  | ||||
|         final var saved = partnerRepo.save(current); | ||||
|         optionallyCreateExPartnerRelation(saved, previousPartnerRel); | ||||
|         final var saved = rbacPartnerRepo.save(current); | ||||
|         optionallyCreateExPartnerRelation(saved, previousPartnerPerson); | ||||
|         optionallyUpdateRelatedRelations(saved, previousPartnerPerson); | ||||
|  | ||||
|         final var mapped = mapper.map(saved, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); | ||||
|         return ResponseEntity.ok(mapped); | ||||
|     } | ||||
|  | ||||
|     private void optionallyCreateExPartnerRelation(final HsOfficePartnerRbacEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) { | ||||
|         if (!saved.getPartnerRel().getUuid().equals(previousPartnerRel.getUuid())) { | ||||
|             // TODO.impl: we also need to use the new partner-person as the anchor | ||||
|             relationRepo.save(previousPartnerRel.toBuilder().uuid(null).type(EX_PARTNER).build()); | ||||
|     private void optionallyCreateExPartnerRelation(final HsOfficePartnerRbacEntity saved, final HsOfficePersonRealEntity previousPartnerPerson) { | ||||
|  | ||||
|         final var partnerPersonHasChanged = !saved.getPartnerRel().getHolder().getUuid().equals(previousPartnerPerson.getUuid()); | ||||
|         if (partnerPersonHasChanged) { | ||||
|             realRelationRepo.save(saved.getPartnerRel().toBuilder() | ||||
|                     .uuid(null) | ||||
|                     .type(EX_PARTNER) | ||||
|                     .anchor(saved.getPartnerRel().getHolder()) | ||||
|                     .holder(previousPartnerPerson) | ||||
|                     .build()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void optionallyUpdateRelatedRelations(final HsOfficePartnerRbacEntity saved, final HsOfficePersonRealEntity previousPartnerPerson) { | ||||
|         final var partnerPersonHasChanged = !saved.getPartnerRel().getHolder().getUuid().equals(previousPartnerPerson.getUuid()); | ||||
|         if (partnerPersonHasChanged) { | ||||
|             // self-debitors of the old partner-person become self-debitors of the new partner person | ||||
|             em.createNativeQuery(""" | ||||
|                 UPDATE hs_office.relation | ||||
|                     SET holderUuid = :newPartnerPersonUuid | ||||
|                     WHERE type = 'DEBITOR' AND | ||||
|                           holderUuid = :oldPartnerPersonUuid AND anchorUuid = :oldPartnerPersonUuid | ||||
|             """) | ||||
|                     .setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid()) | ||||
|                     .setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid()) | ||||
|                     .executeUpdate(); | ||||
|  | ||||
|             // re-anchor all relations from the old partner person to the new partner persion | ||||
|             em.createNativeQuery(""" | ||||
|                 UPDATE hs_office.relation | ||||
|                     SET anchorUuid = :newPartnerPersonUuid | ||||
|                     WHERE anchorUuid = :oldPartnerPersonUuid | ||||
|             """) | ||||
|                     .setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid()) | ||||
|                     .setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid()) | ||||
|                     .executeUpdate(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,35 +1,36 @@ | ||||
| package net.hostsharing.hsadminng.hs.office.partner; | ||||
|  | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource; | ||||
| import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; | ||||
| import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationPatcher; | ||||
| import net.hostsharing.hsadminng.mapper.EntityPatcher; | ||||
| import net.hostsharing.hsadminng.mapper.OptionalFromJson; | ||||
| import net.hostsharing.hsadminng.mapper.StrictMapper; | ||||
|  | ||||
| import jakarta.persistence.EntityManager; | ||||
|  | ||||
| class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatchResource> { | ||||
|  | ||||
|     private final StrictMapper mapper; | ||||
|     private final EntityManager em; | ||||
|     private final HsOfficePartnerRbacEntity entity; | ||||
|  | ||||
|     HsOfficePartnerEntityPatcher( | ||||
|             final StrictMapper mapper, | ||||
|             final EntityManager em, | ||||
|             final HsOfficePartnerRbacEntity entity) { | ||||
|         this.mapper = mapper; | ||||
|         this.em = em; | ||||
|         this.entity = entity; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void apply(final HsOfficePartnerPatchResource resource) { | ||||
|         OptionalFromJson.of(resource.getPartnerRelUuid()).ifPresent(newValue -> { | ||||
|             verifyNotNull(newValue, "partnerRel"); | ||||
|             entity.setPartnerRel(em.getReference(HsOfficeRelationRealEntity.class, newValue)); | ||||
|         }); | ||||
|  | ||||
|         new HsOfficePartnerDetailsEntityPatcher(em, entity.getDetails()).apply(resource.getDetails()); | ||||
|     } | ||||
|         if (resource.getPartnerRel() != null) { | ||||
|             new HsOfficeRelationPatcher(mapper, em, entity.getPartnerRel()).apply(resource.getPartnerRel()); | ||||
|         } | ||||
|  | ||||
|     private void verifyNotNull(final Object newValue, final String propertyName) { | ||||
|         if (newValue == null) { | ||||
|             throw new IllegalArgumentException("property '" + propertyName + "' must not be null"); | ||||
|         if (resource.getDetails() != null) { | ||||
|             new HsOfficePartnerDetailsEntityPatcher(em, entity.getDetails()).apply(resource.getDetails()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -163,13 +163,13 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi { | ||||
|             final String currentSubject, | ||||
|             final String assumedRoles, | ||||
|             final UUID relationUuid, | ||||
|             final HsOfficeRelationPatchResource body) { | ||||
|             final HsOfficeRelationContactPatchResource body) { | ||||
|  | ||||
|         context.define(currentSubject, assumedRoles); | ||||
|  | ||||
|         final var current = rbacRelationRepo.findByUuid(relationUuid).orElseThrow(); | ||||
|  | ||||
|         new HsOfficeRelationEntityPatcher(em, current).apply(body); | ||||
|         new HsOfficeRelationEntityContactPatcher(em, current).apply(body); | ||||
|  | ||||
|         final var saved = rbacRelationRepo.save(current); | ||||
|         final var mapped = mapper.map(saved, HsOfficeRelationResource.class); | ||||
|   | ||||
| @@ -1,25 +1,25 @@ | ||||
| package net.hostsharing.hsadminng.hs.office.relation; | ||||
| 
 | ||||
| import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationContactPatchResource; | ||||
| import net.hostsharing.hsadminng.mapper.EntityPatcher; | ||||
| import net.hostsharing.hsadminng.mapper.OptionalFromJson; | ||||
| 
 | ||||
| import jakarta.persistence.EntityManager; | ||||
| import java.util.UUID; | ||||
| 
 | ||||
| class HsOfficeRelationEntityPatcher implements EntityPatcher<HsOfficeRelationPatchResource> { | ||||
| public class HsOfficeRelationEntityContactPatcher implements EntityPatcher<HsOfficeRelationContactPatchResource> { | ||||
| 
 | ||||
|     private final EntityManager em; | ||||
|     private final HsOfficeRelation entity; | ||||
| 
 | ||||
|     HsOfficeRelationEntityPatcher(final EntityManager em, final HsOfficeRelation entity) { | ||||
|     public HsOfficeRelationEntityContactPatcher(final EntityManager em, final HsOfficeRelation entity) { | ||||
|         this.em = em; | ||||
|         this.entity = entity; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void apply(final HsOfficeRelationPatchResource resource) { | ||||
|     public void apply(final HsOfficeRelationContactPatchResource resource) { | ||||
|         OptionalFromJson.of(resource.getContactUuid()).ifPresent(newValue -> { | ||||
|             verifyNotNull(newValue, "contact"); | ||||
|             entity.setContact(em.getReference(HsOfficeContactRealEntity.class, newValue)); | ||||
| @@ -0,0 +1,50 @@ | ||||
| package net.hostsharing.hsadminng.hs.office.relation; | ||||
|  | ||||
| import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource; | ||||
| import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity; | ||||
| import net.hostsharing.hsadminng.mapper.EntityPatcher; | ||||
| import net.hostsharing.hsadminng.mapper.StrictMapper; | ||||
|  | ||||
| import jakarta.persistence.EntityManager; | ||||
| import jakarta.validation.ValidationException; | ||||
|  | ||||
| public class HsOfficeRelationPatcher implements EntityPatcher<HsOfficeRelationPatchResource> { | ||||
|  | ||||
|     private final StrictMapper mapper; | ||||
|     private final EntityManager em; | ||||
|     private final HsOfficeRelation entity; | ||||
|  | ||||
|     public HsOfficeRelationPatcher(final StrictMapper mapper, final EntityManager em, final HsOfficeRelation entity) { | ||||
|         this.mapper = mapper; | ||||
|         this.em = em; | ||||
|         this.entity = entity; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void apply(final HsOfficeRelationPatchResource resource) { | ||||
|         if (resource.getHolder() != null && resource.getHolderUuid() != null) { | ||||
|             throw new ValidationException("either \"holder\" or \"holder.uuid\" can be given, not both"); | ||||
|         } else { | ||||
|             if (resource.getHolder() != null) { | ||||
|                 final var newHolder = mapper.map(resource.getHolder(), HsOfficePersonRealEntity.class); | ||||
|                 em.persist(newHolder); | ||||
|                 entity.setHolder(newHolder); | ||||
|             } else if (resource.getHolderUuid() != null) { | ||||
|                 entity.setHolder(em.getReference(HsOfficePersonRealEntity.class, resource.getHolderUuid().get())); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (resource.getContact() != null && resource.getContactUuid() != null) { | ||||
|             throw new ValidationException("either \"contact\" or \"contact.uuid\" can be given, not both"); | ||||
|         } else { | ||||
|             if (resource.getContact() != null) { | ||||
|                 final var newContact = mapper.map(resource.getContact(), HsOfficeContactRealEntity.class); | ||||
|                 em.persist(newContact); | ||||
|                 entity.setContact(newContact); | ||||
|             } else if (resource.getContactUuid() != null) { | ||||
|                 entity.setContact(em.getReference(HsOfficeContactRealEntity.class, resource.getContactUuid().get())); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -51,7 +51,7 @@ public class HsOfficeRelationRbacEntity extends HsOfficeRelation { | ||||
|                         """)) | ||||
|                 .withRestrictedViewOrderBy(SQL.expression( | ||||
|                         "(select idName from hs_office.person_iv p where p.uuid = target.holderUuid)")) | ||||
|                 .withUpdatableColumns("contactUuid") | ||||
|                 .withUpdatableColumns("anchorUuid", "holderUuid", "contactUuid") // BEWARE: additional checks at API-level | ||||
|                 .importEntityAlias("anchorPerson", HsOfficePersonRbacEntity.class, usingDefaultCase(), | ||||
|                         dependsOnColumn("anchorUuid"), | ||||
|                         directlyFetchedByDependsOnColumn(), | ||||
|   | ||||
| @@ -48,12 +48,11 @@ components: | ||||
|         HsOfficePartnerPatch: | ||||
|             type: object | ||||
|             properties: | ||||
|                 partnerRel.uuid: | ||||
|                     type: string | ||||
|                     format: uuid | ||||
|                     nullable: true | ||||
|                 partnerRel: | ||||
|                     $ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationPatch' | ||||
|                 details: | ||||
|                     $ref: '#/components/schemas/HsOfficePartnerDetailsPatch' | ||||
|             additionalProperties: false | ||||
|  | ||||
|         HsOfficePartnerDetailsPatch: | ||||
|             type: object | ||||
|   | ||||
| @@ -34,7 +34,7 @@ components: | ||||
|                 contact: | ||||
|                     $ref: 'hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact' | ||||
|  | ||||
|         HsOfficeRelationPatch: | ||||
|         HsOfficeRelationContactPatch: | ||||
|             type: object | ||||
|             properties: | ||||
|                 contact.uuid: | ||||
| @@ -42,6 +42,27 @@ components: | ||||
|                     format: uuid | ||||
|                     nullable: true | ||||
|  | ||||
|         HsOfficeRelationPatch: | ||||
|             type: object | ||||
|             properties: | ||||
|                 anchor.uuid: | ||||
|                     type: string | ||||
|                     format: uuid | ||||
|                     nullable: true | ||||
|                 holder.uuid: | ||||
|                     type: string | ||||
|                     format: uuid | ||||
|                     nullable: true | ||||
|                 holder: | ||||
|                     $ref: 'hs-office-person-schemas.yaml#/components/schemas/HsOfficePersonInsert' | ||||
|                 contact.uuid: | ||||
|                     type: string | ||||
|                     format: uuid | ||||
|                     nullable: true | ||||
|                 contact: | ||||
|                     $ref: 'hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContactInsert' | ||||
|             additionalProperties: false | ||||
|  | ||||
|         # arbitrary relation with explicit type | ||||
|         HsOfficeRelationInsert: | ||||
|             type: object | ||||
|   | ||||
| @@ -44,7 +44,7 @@ patch: | ||||
|         content: | ||||
|             'application/json': | ||||
|                 schema: | ||||
|                     $ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationPatch' | ||||
|                     $ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationContactPatch' | ||||
|     responses: | ||||
|         "200": | ||||
|             description: OK | ||||
|   | ||||
| @@ -124,7 +124,9 @@ create or replace procedure hs_office.relation_update_rbac_system( | ||||
|     language plpgsql as $$ | ||||
| begin | ||||
|  | ||||
|     if NEW.contactUuid is distinct from OLD.contactUuid then | ||||
|     if NEW.holderUuid is distinct from OLD.holderUuid | ||||
|     or NEW.anchorUuid is distinct from OLD.anchorUuid | ||||
|     or NEW.contactUuid is distinct from OLD.contactUuid then | ||||
|         delete from rbac.grant g where g.grantedbytriggerof = OLD.uuid; | ||||
|         call hs_office.relation_build_rbac_system(NEW); | ||||
|     end if; | ||||
| @@ -248,6 +250,8 @@ call rbac.generateRbacRestrictedView('hs_office.relation', | ||||
|         (select idName from hs_office.person_iv p where p.uuid = target.holderUuid) | ||||
|     $orderBy$, | ||||
|     $updates$ | ||||
|         anchorUuid = new.anchorUuid, | ||||
|         holderUuid = new.holderUuid, | ||||
|         contactUuid = new.contactUuid | ||||
|     $updates$); | ||||
| --// | ||||
|   | ||||
| @@ -20,6 +20,7 @@ import org.springframework.boot.test.context.SpringBootTest; | ||||
| import org.springframework.data.repository.Repository; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
| import jakarta.annotation.PostConstruct; | ||||
| import jakarta.persistence.Table; | ||||
|  | ||||
| import java.lang.annotation.Annotation; | ||||
| @@ -420,7 +421,7 @@ public class ArchitectureTest { | ||||
|                     if (isGeneratedSpringRepositoryMethod(item, method)) { | ||||
|                         continue; | ||||
|                     } | ||||
|                     if (item.isAnnotatedWith(RestController.class) && !method.getModifiers().contains(PUBLIC)) { | ||||
|                     if (!method.getModifiers().contains(PUBLIC) || method.isAnnotatedWith(PostConstruct.class)) { | ||||
|                         continue; | ||||
|                     } | ||||
|                     final var message = String.format( | ||||
|   | ||||
| @@ -316,7 +316,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu | ||||
|  | ||||
|             context.define("superuser-alex@hostsharing.net"); | ||||
|             final var givenPartner = givenSomeTemporaryPartnerBessler(20011); | ||||
|             final var givenPartnerRel = givenSomeTemporaryPartnerRel("Winkler", "third contact"); | ||||
|             final var newPartnerPerson = personRealRepo.findPersonByOptionalNameLike("Winkler").getFirst(); | ||||
|  | ||||
|             RestAssured // @formatter:off | ||||
|                 .given() | ||||
| @@ -325,7 +325,9 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu | ||||
|                     .body(""" | ||||
|                            { | ||||
|                                "partnerNumber": "P-20011", | ||||
|                                "partnerRel.uuid": "%s", | ||||
|                                "partnerRel": { | ||||
|                                    "holder.uuid": "%s" | ||||
|                                }, | ||||
|                                "details": { | ||||
|                                    "registrationOffice": "Temp Registergericht Aurich", | ||||
|                                    "registrationNumber": "222222", | ||||
| @@ -334,7 +336,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu | ||||
|                                    "dateOfDeath": "2022-01-12" | ||||
|                                } | ||||
|                              } | ||||
|                            """.formatted(givenPartnerRel.getUuid())) | ||||
|                            """.formatted(newPartnerPerson.getUuid())) | ||||
|                     .port(port) | ||||
|                 .when() | ||||
|                     .patch("http://localhost/api/hs/office/partners/" + givenPartner.getUuid()) | ||||
| @@ -348,7 +350,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu | ||||
|                             "anchor": { "tradeName": "Hostsharing eG" }, | ||||
|                             "holder": { "familyName": "Winkler" }, | ||||
|                             "type": "PARTNER", | ||||
|                             "contact": { "caption": "third contact" } | ||||
|                             "contact": { "caption": "fourth contact" } | ||||
|                         }, | ||||
|                         "details": { | ||||
|                             "registrationOffice": "Temp Registergericht Aurich", | ||||
| @@ -368,7 +370,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu | ||||
|                     .matches(partner -> { | ||||
|                         assertThat(partner.getPartnerNumber()).isEqualTo(givenPartner.getPartnerNumber()); | ||||
|                         assertThat(partner.getPartnerRel().getHolder().getFamilyName()).isEqualTo("Winkler"); | ||||
|                         assertThat(partner.getPartnerRel().getContact().getCaption()).isEqualTo("third contact"); | ||||
|                         assertThat(partner.getPartnerRel().getContact().getCaption()).isEqualTo("fourth contact"); | ||||
|                         assertThat(partner.getDetails().getRegistrationOffice()).isEqualTo("Temp Registergericht Aurich"); | ||||
|                         assertThat(partner.getDetails().getRegistrationNumber()).isEqualTo("222222"); | ||||
|                         assertThat(partner.getDetails().getBirthName()).isEqualTo("Maja Schmidt"); | ||||
| @@ -379,11 +381,11 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu | ||||
|         } | ||||
|  | ||||
|         @Test | ||||
|         void patchingThePartnerRelCreatesExPartnerRel() { | ||||
|         void patchingThePartnerPersonCreatesExPartnerRel() { | ||||
|  | ||||
|             context.define("superuser-alex@hostsharing.net"); | ||||
|             final var givenPartner = givenSomeTemporaryPartnerBessler(20011); | ||||
|             final var givenPartnerRel = givenSomeTemporaryPartnerRel("Winkler", "third contact"); | ||||
|             final var newPartnerPerson = personRealRepo.findPersonByOptionalNameLike("Winkler").getFirst(); | ||||
|  | ||||
|             RestAssured // @formatter:off | ||||
|                     .given() | ||||
| @@ -391,9 +393,11 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu | ||||
|                         .contentType(ContentType.JSON) | ||||
|                         .body(""" | ||||
|                                 { | ||||
|                                    "partnerRel.uuid": "%s" | ||||
|                                    "partnerRel": { | ||||
|                                        "holder.uuid": "%s" | ||||
|                                    } | ||||
|                                 } | ||||
|                                 """.formatted(givenPartnerRel.getUuid())) | ||||
|                                 """.formatted(newPartnerPerson.getUuid())) | ||||
|                         .port(port) | ||||
|                     .when() | ||||
|                         .patch("http://localhost/api/hs/office/partners/" + givenPartner.getUuid()) | ||||
| @@ -405,16 +409,16 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu | ||||
|             context.define("superuser-alex@hostsharing.net"); | ||||
|             assertThat(partnerRepo.findByUuid(givenPartner.getUuid())).isPresent().get() | ||||
|                     .matches(partner -> { | ||||
|                         assertThat(partner.getPartnerRel().getHolder().getFamilyName()).isEqualTo("Winkler"); | ||||
|                         assertThat(partner.getPartnerRel().getContact().getCaption()).isEqualTo("third contact"); | ||||
|                         assertThat(partner.getPartnerRel().getHolder().getFamilyName()).isEqualTo("Winkler"); // updated | ||||
|                         assertThat(partner.getPartnerRel().getContact().getCaption()).isEqualTo("fourth contact"); // unchanged | ||||
|                         return true; | ||||
|                     }); | ||||
|  | ||||
|             // and an ex-partner-relation got created | ||||
|             final var anchorpartnerPersonUUid = givenPartner.getPartnerRel().getAnchor().getUuid(); | ||||
|             assertThat(relationRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(anchorpartnerPersonUUid, EX_PARTNER, null, null, null)) | ||||
|             final var newPartnerPersonUuid = givenPartner.getPartnerRel().getHolder().getUuid(); | ||||
|             assertThat(relationRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(newPartnerPersonUuid, EX_PARTNER, null, null, null)) | ||||
|                     .map(HsOfficeRelation::toShortString) | ||||
|                     .contains("rel(anchor='LP Hostsharing eG', type='EX_PARTNER', holder='UF Erben Bessler')"); | ||||
|                     .contains("rel(anchor='NP Winkler, Paul', type='EX_PARTNER', holder='UF Erben Bessler')"); | ||||
|         } | ||||
|  | ||||
|         @Test | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package net.hostsharing.hsadminng.hs.office.partner; | ||||
|  | ||||
| import net.hostsharing.hsadminng.context.Context; | ||||
| import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter; | ||||
| import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRbacEntity; | ||||
| import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity; | ||||
| import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; | ||||
| @@ -38,7 +39,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. | ||||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||||
|  | ||||
| @WebMvcTest(HsOfficePartnerController.class) | ||||
| @Import({StrictMapper.class, DisableSecurityConfig.class}) | ||||
| @Import({ StrictMapper.class, HsOfficeContactFromResourceConverter.class, DisableSecurityConfig.class}) | ||||
| @ActiveProfiles("test") | ||||
| class HsOfficePartnerControllerRestTest { | ||||
|  | ||||
| @@ -108,8 +109,6 @@ class HsOfficePartnerControllerRestTest { | ||||
|                                              "holder.uuid": "%s", | ||||
|                                              "contact.uuid": "%s" | ||||
|                                         }, | ||||
|                                         "person.uuid": "%s", | ||||
|                                         "contact.uuid": "%s", | ||||
|                                         "details": { | ||||
|                                             "registrationOffice": "Temp Registergericht Aurich", | ||||
|                                             "registrationNumber": "111111" | ||||
| @@ -118,8 +117,6 @@ class HsOfficePartnerControllerRestTest { | ||||
|                                     """.formatted( | ||||
|                                     GIVEN_MANDANTE_UUID, | ||||
|                                     GIVEN_INVALID_UUID, | ||||
|                                     GIVEN_CONTACT_UUID, | ||||
|                                     GIVEN_INVALID_UUID, | ||||
|                                     GIVEN_CONTACT_UUID)) | ||||
|                             .accept(MediaType.APPLICATION_JSON)) | ||||
|  | ||||
| @@ -145,8 +142,6 @@ class HsOfficePartnerControllerRestTest { | ||||
|                                              "holder.uuid": "%s", | ||||
|                                              "contact.uuid": "%s" | ||||
|                                         }, | ||||
|                                         "person.uuid": "%s", | ||||
|                                         "contact.uuid": "%s", | ||||
|                                         "details": { | ||||
|                                             "registrationOffice": "Temp Registergericht Aurich", | ||||
|                                             "registrationNumber": "111111" | ||||
| @@ -155,8 +150,6 @@ class HsOfficePartnerControllerRestTest { | ||||
|                                     """.formatted( | ||||
|                                     GIVEN_MANDANTE_UUID, | ||||
|                                     GIVEN_PERSON_UUID, | ||||
|                                     GIVEN_INVALID_UUID, | ||||
|                                     GIVEN_PERSON_UUID, | ||||
|                                     GIVEN_INVALID_UUID)) | ||||
|                             .accept(MediaType.APPLICATION_JSON)) | ||||
|  | ||||
|   | ||||
| @@ -1,20 +1,25 @@ | ||||
| package net.hostsharing.hsadminng.hs.office.partner; | ||||
|  | ||||
| import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerDetailsPatchResource; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource; | ||||
| import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity; | ||||
| import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; | ||||
| import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; | ||||
| import net.hostsharing.hsadminng.mapper.StrictMapper; | ||||
| import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; | ||||
| import org.junit.jupiter.api.BeforeEach; | ||||
| import org.junit.jupiter.api.Test; | ||||
| import org.junit.jupiter.api.TestInstance; | ||||
| import org.junit.jupiter.api.extension.ExtendWith; | ||||
| import org.mockito.Mock; | ||||
| import org.mockito.junit.jupiter.MockitoExtension; | ||||
| import org.openapitools.jackson.nullable.JsonNullable; | ||||
|  | ||||
| import jakarta.persistence.EntityManager; | ||||
| import java.time.LocalDate; | ||||
| import java.util.UUID; | ||||
| import java.util.stream.Stream; | ||||
|  | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
| import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; | ||||
| import static org.mockito.ArgumentMatchers.any; | ||||
| import static org.mockito.ArgumentMatchers.eq; | ||||
| @@ -22,19 +27,17 @@ import static org.mockito.Mockito.lenient; | ||||
|  | ||||
| @TestInstance(PER_CLASS) | ||||
| @ExtendWith(MockitoExtension.class) | ||||
| class HsOfficePartnerEntityPatcherUnitTest extends PatchUnitTestBase< | ||||
|         HsOfficePartnerPatchResource, | ||||
|         HsOfficePartnerRbacEntity | ||||
|         > { | ||||
| // This test class does not subclass PatchUnitTestBase because it has no directly patchable properties. | ||||
| // But the factory-structure is kept, so PatchUnitTestBase could easily be plugged back in if needed. | ||||
| class HsOfficePartnerEntityPatcherUnitTest { | ||||
|  | ||||
|     private static final UUID INITIAL_PARTNER_UUID = UUID.randomUUID(); | ||||
|     private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID(); | ||||
|     private static final UUID INITIAL_PERSON_UUID = UUID.randomUUID(); | ||||
|     private static final UUID INITIAL_PARTNER_PERSON_UUID = UUID.randomUUID(); | ||||
|     private static final UUID INITIAL_DETAILS_UUID = UUID.randomUUID(); | ||||
|     private static final UUID PATCHED_PARTNER_ROLE_UUID = UUID.randomUUID(); | ||||
|  | ||||
|     private final HsOfficePersonRealEntity givenInitialPerson = HsOfficePersonRealEntity.builder() | ||||
|             .uuid(INITIAL_PERSON_UUID) | ||||
|     private final HsOfficePersonRealEntity givenInitialPartnerPerson = HsOfficePersonRealEntity.builder() | ||||
|             .uuid(INITIAL_PARTNER_PERSON_UUID) | ||||
|             .build(); | ||||
|     private final HsOfficeContactRealEntity givenInitialContact = HsOfficeContactRealEntity.builder() | ||||
|             .uuid(INITIAL_CONTACT_UUID) | ||||
| @@ -43,22 +46,74 @@ class HsOfficePartnerEntityPatcherUnitTest extends PatchUnitTestBase< | ||||
|     private final HsOfficePartnerDetailsEntity givenInitialDetails = HsOfficePartnerDetailsEntity.builder() | ||||
|             .uuid(INITIAL_DETAILS_UUID) | ||||
|             .build(); | ||||
|  | ||||
|     @Mock | ||||
|     private EntityManager em; | ||||
|     private EntityManagerWrapper emw; | ||||
|  | ||||
|     private StrictMapper mapper = new StrictMapper(emw); | ||||
|  | ||||
|     @BeforeEach | ||||
|     void initMocks() { | ||||
|         lenient().when(em.getReference(eq(HsOfficeRelationRealEntity.class), any())).thenAnswer(invocation -> | ||||
|                 HsOfficeRelationRealEntity.builder().uuid(invocation.getArgument(1)).build()); | ||||
|         lenient().when(emw.getReference(eq(HsOfficePersonRealEntity.class), any())).thenAnswer(invocation -> | ||||
|                 HsOfficePersonRealEntity.builder().uuid(invocation.getArgument(1)).build()); | ||||
|         lenient().when(emw.getReference(eq(HsOfficeContactRealEntity.class), any())).thenAnswer(invocation -> | ||||
|                 HsOfficeContactRealEntity.builder().uuid(invocation.getArgument(1)).build()); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void patchPartnerPerson() { | ||||
|         // given | ||||
|         final var patchResource = newPatchResource(); | ||||
|         final var newHolderUuid = UUID.randomUUID(); | ||||
|         patchResource.setPartnerRel(new HsOfficeRelationPatchResource()); | ||||
|         patchResource.getPartnerRel().setHolderUuid(JsonNullable.of(newHolderUuid)); | ||||
|         final var entity = newInitialEntity(); | ||||
|  | ||||
|         // when | ||||
|         createPatcher(entity).apply(patchResource); | ||||
|  | ||||
|         // then | ||||
|         assertThat(entity.getPartnerRel().getHolder().getUuid()).isEqualTo(newHolderUuid); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void patchPartnerContact() { | ||||
|         // given | ||||
|         final var patchResource = newPatchResource(); | ||||
|         final var newContactUuid = UUID.randomUUID(); | ||||
|         patchResource.setPartnerRel(new HsOfficeRelationPatchResource()); | ||||
|         patchResource.getPartnerRel().setContactUuid(JsonNullable.of(newContactUuid)); | ||||
|         final var entity = newInitialEntity(); | ||||
|  | ||||
|         // when | ||||
|         createPatcher(entity).apply(patchResource); | ||||
|  | ||||
|         // then | ||||
|         assertThat(entity.getPartnerRel().getContact().getUuid()).isEqualTo(newContactUuid); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void patchPartnerDetails() { | ||||
|         // given | ||||
|         final var patchResource = newPatchResource(); | ||||
|         final var newDateOfBirth = LocalDate.now(); | ||||
|         patchResource.setDetails(new HsOfficePartnerDetailsPatchResource()); | ||||
|         patchResource.getDetails().setDateOfDeath(JsonNullable.of(newDateOfBirth)); | ||||
|         final var entity = newInitialEntity(); | ||||
|  | ||||
|         // when | ||||
|         createPatcher(entity).apply(patchResource); | ||||
|  | ||||
|         // then | ||||
|         assertThat(entity.getDetails().getDateOfDeath()).isEqualTo(newDateOfBirth); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected HsOfficePartnerRbacEntity newInitialEntity() { | ||||
|         final var entity = HsOfficePartnerRbacEntity.builder() | ||||
|                 .uuid(INITIAL_PARTNER_UUID) | ||||
|                 .partnerNumber(12345) | ||||
|                 .partnerRel(HsOfficeRelationRealEntity.builder() | ||||
|                         .holder(givenInitialPerson) | ||||
|                         .holder(givenInitialPartnerPerson) | ||||
|                         .contact(givenInitialContact) | ||||
|                         .build()) | ||||
|                 .details(givenInitialDetails) | ||||
| @@ -66,32 +121,11 @@ class HsOfficePartnerEntityPatcherUnitTest extends PatchUnitTestBase< | ||||
|         return entity; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected HsOfficePartnerPatchResource newPatchResource() { | ||||
|         return new HsOfficePartnerPatchResource(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected HsOfficePartnerEntityPatcher createPatcher(final HsOfficePartnerRbacEntity partner) { | ||||
|         return new HsOfficePartnerEntityPatcher(em, partner); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected Stream<Property> propertyTestDescriptors() { | ||||
|         return Stream.of( | ||||
|                 new JsonNullableProperty<>( | ||||
|                         "partnerRel", | ||||
|                         HsOfficePartnerPatchResource::setPartnerRelUuid, | ||||
|                         PATCHED_PARTNER_ROLE_UUID, | ||||
|                         HsOfficePartnerRbacEntity::setPartnerRel, | ||||
|                         newPartnerRel(PATCHED_PARTNER_ROLE_UUID)) | ||||
|                         .notNullable() | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private static HsOfficeRelationRealEntity newPartnerRel(final UUID uuid) { | ||||
|         return HsOfficeRelationRealEntity.builder() | ||||
|                     .uuid(uuid) | ||||
|                     .build(); | ||||
|         return new HsOfficePartnerEntityPatcher(mapper, emw, partner); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,23 +1,38 @@ | ||||
| package net.hostsharing.hsadminng.hs.office.relation; | ||||
|  | ||||
| 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.model.HsOfficeContactInsertResource; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonTypeResource; | ||||
| import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource; | ||||
| import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity; | ||||
| import net.hostsharing.hsadminng.mapper.StrictMapper; | ||||
| import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; | ||||
| import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; | ||||
| import org.junit.jupiter.api.BeforeEach; | ||||
| import org.junit.jupiter.api.Test; | ||||
| import org.junit.jupiter.api.TestInstance; | ||||
| import org.junit.jupiter.api.extension.ExtendWith; | ||||
| import org.mockito.Mock; | ||||
| import org.mockito.junit.jupiter.MockitoExtension; | ||||
| import org.openapitools.jackson.nullable.JsonNullable; | ||||
|  | ||||
| import jakarta.persistence.EntityManager; | ||||
| import jakarta.validation.ValidationException; | ||||
| import java.util.Map; | ||||
| import java.util.UUID; | ||||
| import java.util.stream.Stream; | ||||
|  | ||||
| import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.LEGAL_PERSON; | ||||
| import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.NATURAL_PERSON; | ||||
| import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.PARTNER; | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
| import static org.assertj.core.api.Assertions.catchThrowable; | ||||
| import static org.assertj.core.api.Assumptions.assumeThat; | ||||
| import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; | ||||
| import static org.mockito.ArgumentMatchers.any; | ||||
| import static org.mockito.ArgumentMatchers.eq; | ||||
| import static org.mockito.Mockito.lenient; | ||||
| import static org.mockito.Mockito.times; | ||||
| import static org.mockito.Mockito.verify; | ||||
|  | ||||
| @TestInstance(PER_CLASS) | ||||
| @ExtendWith(MockitoExtension.class) | ||||
| @@ -26,36 +41,84 @@ class HsOfficeRelationPatcherUnitTest extends PatchUnitTestBase< | ||||
|         HsOfficeRelation | ||||
|         > { | ||||
|  | ||||
|     static final UUID INITIAL_RELATION_UUID = UUID.randomUUID(); | ||||
|     static final UUID PATCHED_CONTACT_UUID = UUID.randomUUID(); | ||||
|     private static final UUID INITIAL_RELATION_UUID = UUID.randomUUID(); | ||||
|     private static final UUID INITIAL_ANCHOR_UUID = UUID.randomUUID(); | ||||
|     private static final UUID INITIAL_HOLDER_UUID = UUID.randomUUID(); | ||||
|     private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID(); | ||||
|  | ||||
|     private static final UUID PATCHED_HOLDER_UUID = UUID.randomUUID(); | ||||
|     private static HsOfficePersonInsertResource HOLDER_PATCH_RESOURCE = new HsOfficePersonInsertResource(); | ||||
|     { | ||||
|         { | ||||
|             HOLDER_PATCH_RESOURCE.setPersonType(HsOfficePersonTypeResource.NATURAL_PERSON); | ||||
|             HOLDER_PATCH_RESOURCE.setFamilyName("Patched-Holder-Family-Name"); | ||||
|             HOLDER_PATCH_RESOURCE.setGivenName("Patched-Holder-Given-Name"); | ||||
|         } | ||||
|     }; | ||||
|     private static HsOfficePersonRealEntity PATCHED_HOLDER = HsOfficePersonRealEntity.builder() | ||||
|             .uuid(PATCHED_HOLDER_UUID) | ||||
|             .personType(NATURAL_PERSON) | ||||
|             .familyName("Patched-Holder-Family-Name") | ||||
|             .givenName("Patched-Holder-Given-Name") | ||||
|             .build(); | ||||
|  | ||||
|     private static final UUID PATCHED_CONTACT_UUID = UUID.randomUUID(); | ||||
|     private static HsOfficeContactInsertResource CONTACT_PATCH_RESOURCE = new HsOfficeContactInsertResource(); | ||||
|     { | ||||
|         { | ||||
|             CONTACT_PATCH_RESOURCE.setCaption("Patched-Contact-Caption"); | ||||
|             CONTACT_PATCH_RESOURCE.setEmailAddresses(Map.ofEntries( | ||||
|                     Map.entry("main", "patched@example.org") | ||||
|             )); | ||||
|         } | ||||
|     }; | ||||
|     private static HsOfficeContactRealEntity PATCHED_CONTACT = HsOfficeContactRealEntity.builder() | ||||
|             .uuid(PATCHED_CONTACT_UUID) | ||||
|             .caption("Patched-Contact-Caption") | ||||
|             .emailAddresses(Map.ofEntries( | ||||
|                     Map.entry("main", "patched@example.org") | ||||
|             )) | ||||
|             .build(); | ||||
|  | ||||
|     @Mock | ||||
|     EntityManager em; | ||||
|     private EntityManagerWrapper emw; | ||||
|  | ||||
|     private StrictMapper mapper; | ||||
|  | ||||
|  | ||||
|     @BeforeEach | ||||
|     void initMocks() { | ||||
|         lenient().when(em.getReference(eq(HsOfficeContactRealEntity.class), any())).thenAnswer(invocation -> | ||||
|                 HsOfficeContactRealEntity.builder().uuid(invocation.getArgument(1)).build()); | ||||
|     } | ||||
|     void init() { | ||||
|         mapper = new StrictMapper(emw); // emw is injected after the constructor got called | ||||
|         mapper.addConverter( | ||||
|                     new HsOfficeContactFromResourceConverter<>(), | ||||
|                     HsOfficeContactInsertResource.class, HsOfficeContactRealEntity.class); | ||||
|  | ||||
|     final HsOfficePersonRealEntity givenInitialAnchorPerson = HsOfficePersonRealEntity.builder() | ||||
|             .uuid(UUID.randomUUID()) | ||||
|             .build(); | ||||
|     final HsOfficePersonRealEntity givenInitialHolderPerson = HsOfficePersonRealEntity.builder() | ||||
|             .uuid(UUID.randomUUID()) | ||||
|             .build(); | ||||
|     final HsOfficeContactRealEntity givenInitialContact = HsOfficeContactRealEntity.builder() | ||||
|             .uuid(UUID.randomUUID()) | ||||
|             .build(); | ||||
|         lenient().when(emw.getReference(HsOfficePersonRealEntity.class, PATCHED_HOLDER_UUID)).thenAnswer( | ||||
|                 p -> PATCHED_HOLDER); | ||||
|         lenient().when(emw.getReference(HsOfficeContactRealEntity.class, PATCHED_CONTACT_UUID)).thenAnswer( | ||||
|                 p -> PATCHED_CONTACT); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected HsOfficeRelation newInitialEntity() { | ||||
|         final var entity = new HsOfficeRelationRbacEntity(); | ||||
|         final var entity = new HsOfficeRelationRealEntity(); | ||||
|         entity.setUuid(INITIAL_RELATION_UUID); | ||||
|         entity.setType(HsOfficeRelationType.REPRESENTATIVE); | ||||
|         entity.setAnchor(givenInitialAnchorPerson); | ||||
|         entity.setHolder(givenInitialHolderPerson); | ||||
|         entity.setContact(givenInitialContact); | ||||
|         entity.setType(PARTNER); | ||||
|         entity.setAnchor(HsOfficePersonRealEntity.builder() | ||||
|                 .uuid(INITIAL_ANCHOR_UUID) | ||||
|                 .personType(LEGAL_PERSON) | ||||
|                 .tradeName("Initial-Anchor-Tradename") | ||||
|                 .build()); | ||||
|         entity.setHolder(HsOfficePersonRealEntity.builder() | ||||
|                 .uuid(INITIAL_HOLDER_UUID) | ||||
|                 .personType(NATURAL_PERSON) | ||||
|                 .familyName("Initial-Holder-Family-Name") | ||||
|                 .givenName("Initial-Holder-Given-Name") | ||||
|                 .build()); | ||||
|         entity.setContact(HsOfficeContactRealEntity.builder() | ||||
|                 .uuid(INITIAL_CONTACT_UUID) | ||||
|                 .caption("Initial-Contact-Caption") | ||||
|                 .build()); | ||||
|         return entity; | ||||
|     } | ||||
|  | ||||
| @@ -65,24 +128,114 @@ class HsOfficeRelationPatcherUnitTest extends PatchUnitTestBase< | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected HsOfficeRelationEntityPatcher createPatcher(final HsOfficeRelation relation) { | ||||
|         return new HsOfficeRelationEntityPatcher(em, relation); | ||||
|     protected HsOfficeRelationPatcher createPatcher(final HsOfficeRelation relation) { | ||||
|         return new HsOfficeRelationPatcher(mapper, emw, relation); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected Stream<Property> propertyTestDescriptors() { | ||||
|         return Stream.of( | ||||
|                 new JsonNullableProperty<>( | ||||
|                         "contact", | ||||
|                         "holderUuid", | ||||
|                         HsOfficeRelationPatchResource::setHolderUuid, | ||||
|                         PATCHED_HOLDER_UUID, | ||||
|                         HsOfficeRelation::setHolder, | ||||
|                         PATCHED_HOLDER), | ||||
|                 new SimpleProperty<>( | ||||
|                         "holder", | ||||
|                         HsOfficeRelationPatchResource::setHolder, | ||||
|                         HOLDER_PATCH_RESOURCE, | ||||
|                         HsOfficeRelation::setHolder, | ||||
|                         withoutUuid(PATCHED_HOLDER)) | ||||
|                         .notNullable(), | ||||
|  | ||||
|                 new JsonNullableProperty<>( | ||||
|                         "contactUuid", | ||||
|                         HsOfficeRelationPatchResource::setContactUuid, | ||||
|                         PATCHED_CONTACT_UUID, | ||||
|                         HsOfficeRelation::setContact, | ||||
|                         newContact(PATCHED_CONTACT_UUID)) | ||||
|                         PATCHED_CONTACT), | ||||
|                 new SimpleProperty<>( | ||||
|                         "contact", | ||||
|                         HsOfficeRelationPatchResource::setContact, | ||||
|                         CONTACT_PATCH_RESOURCE, | ||||
|                         HsOfficeRelation::setContact, | ||||
|                         withoutUuid(PATCHED_CONTACT)) | ||||
|                         .notNullable() | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     static HsOfficeContactRealEntity newContact(final UUID uuid) { | ||||
|         return HsOfficeContactRealEntity.builder().uuid(uuid).build(); | ||||
|     @Override | ||||
|     protected void willPatchAllProperties() { | ||||
|         // this generic test does not work because either holder or holder.uuid can be set | ||||
|         assumeThat(true).isFalse(); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void willThrowExceptionIfHolderAndHolderUuidAreGiven() { | ||||
|         // given | ||||
|         final var givenEntity = newInitialEntity(); | ||||
|         final var patchResource = newPatchResource(); | ||||
|         patchResource.setHolderUuid(JsonNullable.of(PATCHED_HOLDER_UUID)); | ||||
|         patchResource.setHolder(HOLDER_PATCH_RESOURCE); | ||||
|  | ||||
|         // when | ||||
|         final var exception = catchThrowable(() -> createPatcher(givenEntity).apply(patchResource)); | ||||
|  | ||||
|         // then | ||||
|         assertThat(exception).isInstanceOf(ValidationException.class) | ||||
|                 .hasMessage("either \"holder\" or \"holder.uuid\" can be given, not both"); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void willThrowExceptionIfContactAndContactUuidAreGiven() { | ||||
|         // given | ||||
|         final var givenEntity = newInitialEntity(); | ||||
|         final var patchResource = newPatchResource(); | ||||
|         patchResource.setContactUuid(JsonNullable.of(PATCHED_CONTACT_UUID)); | ||||
|         patchResource.setContact(CONTACT_PATCH_RESOURCE); | ||||
|  | ||||
|         // when | ||||
|         final var exception =  catchThrowable(() -> createPatcher(givenEntity).apply(patchResource)); | ||||
|  | ||||
|         // then | ||||
|         assertThat(exception).isInstanceOf(ValidationException.class) | ||||
|                 .hasMessage("either \"contact\" or \"contact.uuid\" can be given, not both"); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void willPersistNewHolder() { | ||||
|         // given | ||||
|         final var givenEntity = newInitialEntity(); | ||||
|         final var patchResource = newPatchResource(); | ||||
|         patchResource.setHolder(HOLDER_PATCH_RESOURCE); | ||||
|  | ||||
|         // when | ||||
|         createPatcher(givenEntity).apply(patchResource); | ||||
|  | ||||
|         // then | ||||
|         verify(emw, times(1)).persist(givenEntity.getHolder()); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void willPersistNewContact() { | ||||
|         // given | ||||
|         final var givenEntity = newInitialEntity(); | ||||
|         final var patchResource = newPatchResource(); | ||||
|         patchResource.setContact(CONTACT_PATCH_RESOURCE); | ||||
|  | ||||
|         // when | ||||
|         createPatcher(givenEntity).apply(patchResource); | ||||
|  | ||||
|         // then | ||||
|         verify(emw, times(1)).persist(givenEntity.getContact()); | ||||
|     } | ||||
|  | ||||
|     private HsOfficePersonRealEntity withoutUuid(final HsOfficePersonRealEntity givenWithUuid) { | ||||
|         return givenWithUuid.toBuilder().uuid(null).build(); | ||||
|     } | ||||
|  | ||||
|     private HsOfficeContactRealEntity withoutUuid(final HsOfficeContactRealEntity givenWithUuid) { | ||||
|         return givenWithUuid.toBuilder().uuid(null).build(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -28,6 +28,7 @@ import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.NATU | ||||
| import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.UNINCORPORATED_FIRM; | ||||
| import static net.hostsharing.hsadminng.rbac.grant.RawRbacGrantEntity.distinctGrantDisplaysOf; | ||||
| import static net.hostsharing.hsadminng.rbac.role.RawRbacRoleEntity.distinctRoleNamesOf; | ||||
| import static net.hostsharing.hsadminng.rbac.role.RbacRoleType.ADMIN; | ||||
| import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
|  | ||||
| @@ -282,7 +283,40 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea | ||||
|             assertThatRelationIsNotVisibleForUserWithRole( | ||||
|                     result.returnedValue(), | ||||
|                     "hs_office.contact#fifthcontact:ADMIN"); | ||||
|         } | ||||
|  | ||||
|         @Test | ||||
|         public void hostsharingAdmin_withoutAssumedRole_canUpdateHolderOfArbitraryRelation() { | ||||
|             // given | ||||
|             context("superuser-alex@hostsharing.net"); | ||||
|             final var givenRelation = givenSomeTemporaryRelationBessler( | ||||
|                     "Bert", "fifth contact"); | ||||
|             final var oldHolderPerson = givenRelation.getHolder(); | ||||
|             final var newHolderPerson = personRepo.findPersonByOptionalNameLike("Paul").getFirst(); | ||||
|             assertThatRelationActuallyInDatabase(givenRelation); | ||||
|             assertThatRelationIsVisibleForUserWithRole( | ||||
|                     givenRelation, | ||||
|                     givenRelation.getHolder().roleId(ADMIN)); | ||||
|  | ||||
|             // when | ||||
|             final var result = jpaAttempt.transacted(() -> { | ||||
|                 context("superuser-alex@hostsharing.net"); | ||||
|                 givenRelation.setHolder(newHolderPerson); | ||||
|                 return toCleanup(relationRbacRepo.save(givenRelation).load()); | ||||
|             }); | ||||
|  | ||||
|             // then | ||||
|             result.assertSuccessful(); | ||||
|             assertThat(result.returnedValue().getHolder().getGivenName()).isEqualTo("Paul"); | ||||
|             assertThatRelationIsVisibleForUserWithRole( | ||||
|                     result.returnedValue(), | ||||
|                     "rbac.global#global:ADMIN"); | ||||
|             assertThatRelationIsVisibleForUserWithRole( | ||||
|                     result.returnedValue(), | ||||
|                     newHolderPerson.roleId(ADMIN)); | ||||
|             assertThatRelationIsNotVisibleForUserWithRole( | ||||
|                     result.returnedValue(), | ||||
|                     oldHolderPerson.roleId(ADMIN)); | ||||
|         } | ||||
|  | ||||
|         @Test | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.hs.office.scenarios.contact.RemovePhoneNumberFr | ||||
| import net.hostsharing.hsadminng.hs.office.scenarios.contact.ReplaceContactData; | ||||
| import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateExternalDebitorForPartner; | ||||
| import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateSelfDebitorForPartner; | ||||
| import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateSelfDebitorForPartnerWithIdenticalContactData; | ||||
| import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateSepaMandateForDebitor; | ||||
| import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DeleteDebitor; | ||||
| import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DontDeleteDefaultDebitor; | ||||
| @@ -265,11 +266,27 @@ class HsOfficeScenarioTests extends ScenarioTest { | ||||
|     @TestMethodOrder(MethodOrderer.OrderAnnotation.class) | ||||
|     class DebitorScenarios { | ||||
|  | ||||
|         @Test | ||||
|         @Order(2000) | ||||
|         @Requires("Partner: P-31011 - Michelle Matthieu") | ||||
|         @Produces("Debitor: D-3101100 - Michelle Matthieu") | ||||
|         void shouldCreateSelfDebitorForPartnerWithIdenticalContactData() { | ||||
|             new CreateSelfDebitorForPartnerWithIdenticalContactData(scenarioTest) | ||||
|                     .given("partnerNumber", "P-31011") | ||||
|                     .given("debitorNumberSuffix", "00") // TODO.impl: could be assigned automatically, but is not yet | ||||
|                     .given("billable", true) | ||||
|                     .given("vatBusiness", false) | ||||
|                     .given("vatReverseCharge", false) | ||||
|                     .given("defaultPrefix", "mim") | ||||
|                     .doRun() | ||||
|                     .keep(); | ||||
|         } | ||||
|  | ||||
|         @Test | ||||
|         @Order(2010) | ||||
|         @Requires("Partner: P-31010 - Test AG") | ||||
|         @Produces("Debitor: D-3101000 - Test AG - main debitor") | ||||
|         void shouldCreateSelfDebitorForPartner() { | ||||
|         void shouldCreateSelfDebitorForPartnerWithDistinctContactData() { | ||||
|             new CreateSelfDebitorForPartner(scenarioTest) | ||||
|                     .given("partnerPersonTradeName", "Test AG") | ||||
|                     .given("billingContactCaption", "Test AG - billing department") | ||||
| @@ -654,7 +671,7 @@ class HsOfficeScenarioTests extends ScenarioTest { | ||||
|  | ||||
|         @Test | ||||
|         @Order(6010) | ||||
|         @Requires("Partner: P-31011 - Michelle Matthieu") | ||||
|         @Requires("Debitor: D-3101100 - Michelle Matthieu") // which should also get updated | ||||
|         void shouldReplaceDeceasedPartnerByCommunityOfHeirs() { | ||||
|             new ReplaceDeceasedPartnerWithCommunityOfHeirs(scenarioTest) | ||||
|                     .given("partnerNumber", "P-31011") | ||||
|   | ||||
| @@ -0,0 +1,45 @@ | ||||
| package net.hostsharing.hsadminng.hs.office.scenarios.debitor; | ||||
|  | ||||
| import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; | ||||
| import net.hostsharing.hsadminng.hs.scenarios.UseCase; | ||||
| import org.springframework.http.HttpStatus; | ||||
|  | ||||
| import static io.restassured.http.ContentType.JSON; | ||||
| import static org.springframework.http.HttpStatus.CREATED; | ||||
|  | ||||
| public class CreateSelfDebitorForPartnerWithIdenticalContactData | ||||
|         extends UseCase<CreateSelfDebitorForPartnerWithIdenticalContactData> { | ||||
|  | ||||
|     public CreateSelfDebitorForPartnerWithIdenticalContactData(final ScenarioTest testSuite) { | ||||
|         super(testSuite); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected HttpResponse run() { | ||||
|         withTitle("Determine Partner-Person UUID", () -> | ||||
|                 httpGet("/api/hs/office/partners/" + uriEncoded("%{partnerNumber}")) | ||||
|                         .reportWithResponse().expecting(HttpStatus.OK).expecting(JSON) | ||||
|                         .extractUuidAlias("partnerRel.holder.uuid", "partnerPersonUuid") | ||||
|                         .extractUuidAlias("partnerRel.contact.uuid", "partnerContactUuid") | ||||
|         ); | ||||
|  | ||||
|         return httpPost("/api/hs/office/debitors", usingJsonBody(""" | ||||
|                 { | ||||
|                     "debitorRel": { | ||||
|                         "anchor.uuid": ${partnerPersonUuid}, | ||||
|                         "holder.uuid": ${partnerPersonUuid}, | ||||
|                         "contact.uuid": ${partnerContactUuid} | ||||
|                      }, | ||||
|                     "debitorNumberSuffix": ${debitorNumberSuffix}, | ||||
|                     "billable": ${billable}, | ||||
|                     "vatId": ${vatId???}, | ||||
|                     "vatCountryCode": ${vatCountryCode???}, | ||||
|                     "vatBusiness": ${vatBusiness}, | ||||
|                     "vatReverseCharge": ${vatReverseCharge}, | ||||
|                     "defaultPrefix": ${defaultPrefix} | ||||
|                 } | ||||
|                 """)) | ||||
|                 .expecting(CREATED).expecting(JSON); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -12,101 +12,81 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase<ReplaceD | ||||
|  | ||||
|     public ReplaceDeceasedPartnerWithCommunityOfHeirs(final ScenarioTest testSuite) { | ||||
|         super(testSuite); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected HttpResponse run() { | ||||
|  | ||||
|         obtain("Person: Hostsharing eG", () -> | ||||
|             httpGet("/api/hs/office/persons?name=Hostsharing+eG") | ||||
|                     .expecting(OK).expecting(JSON), | ||||
|                     response -> response.expectArrayElements(1).getFromBody("[0].uuid"), | ||||
|                 "Even in production data we expect this query to return just a single result." // TODO.impl: add constraint? | ||||
|         ); | ||||
|  | ||||
|         obtain("Partner: %{partnerNumber}", () -> | ||||
|                 httpGet("/api/hs/office/partners/%{partnerNumber}") | ||||
|                 .reportWithResponse().expecting(OK).expecting(JSON), | ||||
|         obtain("Partner: %{partnerNumber}", | ||||
|                 () -> httpGet("/api/hs/office/partners/%{partnerNumber}") | ||||
|                                 .reportWithResponse().expecting(OK).expecting(JSON), | ||||
|                 response -> response.getFromBody("uuid"), | ||||
|                 "Even in production data we expect this query to return just a single result." // TODO.impl: add constraint? | ||||
|                 "Even in production data we expect this query to return just a single result." | ||||
|                 // TODO.impl: add constraint? | ||||
|         ) | ||||
|                 .extractValue("partnerRel.holder.familyName", "familyNameOfDeceasedPerson") | ||||
|                 .extractValue("partnerRel.holder.givenName", "givenNameOfDeceasedPerson") | ||||
|                 .extractUuidAlias("partnerRel.holder.uuid", "Person: %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"); | ||||
|                 .extractUuidAlias( | ||||
|                         "partnerRel.holder.uuid", | ||||
|                         "Person: %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"); | ||||
|  | ||||
|         obtain("Partner-Relation: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", () -> | ||||
|             httpPost("/api/hs/office/relations", usingJsonBody(""" | ||||
|                 { | ||||
|                    "type": "PARTNER", | ||||
|                    "anchor.uuid": ${Person: Hostsharing eG}, | ||||
|                    "holder": { | ||||
|                         "personType": "UNINCORPORATED_FIRM", | ||||
|                         "tradeName": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", | ||||
|                     }, | ||||
|                    "contact": { | ||||
|                         "caption": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", | ||||
|                         "postalAddress": { | ||||
|                             "name": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", | ||||
|                             "co": "%{representativeGivenName} %{representativeFamilyName}", | ||||
|                             %{communityOfHeirsPostalAddress} | ||||
|                         }, | ||||
|                         "phoneNumbers": { | ||||
|                             "office": ${communityOfHeirsOfficePhoneNumber} | ||||
|                         }, | ||||
|                         "emailAddresses": { | ||||
|                             "main": ${communityOfHeirsEmailAddress} | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 """)) | ||||
|                 .reportWithResponse().expecting(CREATED).expecting(JSON) | ||||
|         ) | ||||
|                 .extractUuidAlias("contact.uuid", "Contact: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}") | ||||
|                 .extractUuidAlias("holder.uuid", "Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"); | ||||
|  | ||||
|         obtain("Representative-Relation: %{representativeGivenName} %{representativeFamilyName} for Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", () -> | ||||
|                 httpPost("/api/hs/office/relations", usingJsonBody(""" | ||||
|                 { | ||||
|                    "type": "REPRESENTATIVE", | ||||
|                    "anchor.uuid": ${Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}, | ||||
|                    "holder": { | ||||
|                         "personType": "NATURAL_PERSON", | ||||
|                         "givenName": ${representativeGivenName}, | ||||
|                         "familyName": ${representativeFamilyName} | ||||
|                     }, | ||||
|                    "contact.uuid": ${Contact: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}} | ||||
|                 } | ||||
|                 """)) | ||||
|                 .reportWithResponse().expecting(CREATED).expecting(JSON) | ||||
|         ).extractUuidAlias("holder.uuid", "Person: %{representativeGivenName} %{representativeFamilyName}"); | ||||
|  | ||||
|         obtain("Partner: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", () -> | ||||
|             httpPatch("/api/hs/office/partners/%{Partner: %{partnerNumber}}", usingJsonBody(""" | ||||
|                         { | ||||
|                             "partnerRel.uuid": ${Partner-Relation: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}} | ||||
|                         } | ||||
|                         """)) | ||||
|                     .expecting(HttpStatus.OK) | ||||
|         withTitle("New Partner-Person+Contact: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", | ||||
|                 () -> httpPatch("/api/hs/office/partners/%{Partner: %{partnerNumber}}", | ||||
|                         usingJsonBody(""" | ||||
|                                 { | ||||
|                                     "wrong1": false, | ||||
|                                     "partnerRel": { | ||||
|                                         "wrong2": false, | ||||
|                                         "holder": { | ||||
|                                             "personType": "UNINCORPORATED_FIRM", | ||||
|                                             "tradeName": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", | ||||
|                                         }, | ||||
|                                         "contact": { | ||||
|                                             "wrong3": false, | ||||
|                                             "caption": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", | ||||
|                                             "postalAddress": { | ||||
|                                                 "wrong4": false, | ||||
|                                                 "name": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", | ||||
|                                                 "co": "%{representativeGivenName} %{representativeFamilyName}", | ||||
|                                                 %{communityOfHeirsPostalAddress} | ||||
|                                             }, | ||||
|                                             "phoneNumbers": { | ||||
|                                                 "office": ${communityOfHeirsOfficePhoneNumber} | ||||
|                                             }, | ||||
|                                             "emailAddresses": { | ||||
|                                                 "main": ${communityOfHeirsEmailAddress} | ||||
|                                             } | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|                                 """)) | ||||
|                         .reportWithResponse().expecting(HttpStatus.OK).expecting(JSON) | ||||
|                         .extractUuidAlias( | ||||
|                                 "partnerRel.holder.uuid", | ||||
|                                 "Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}") | ||||
|                         .extractUuidAlias( | ||||
|                                 "partnerRel.contact.uuid", | ||||
|                                 "Contact: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}") | ||||
|         ); | ||||
|  | ||||
|         // TODO.test: missing steps Debitor, Membership, Coop-Shares+Assets | ||||
|  | ||||
|         // Debitors | ||||
|  | ||||
|         // die Erbengemeinschaft wird als Anchor-Person (Partner) in die Debitor-Relations eingetragen | ||||
|         // der neue Rechnungsempfänger (z.B. auch ggf. Rechtsanwalt) wird als Holder-Person (Debitor-Person) in die Debitor-Relations eingetragen -- oder neu? | ||||
|  | ||||
|         // Membership | ||||
|  | ||||
|         // intro: die Mitgliedschaft geht juristisch gesehen auf die Erbengemeinschaft über | ||||
|  | ||||
|         // die bisherige Mitgliedschaft als DECEASED mit Ende-Datum=Todesdatum markieren | ||||
|  | ||||
|         // eine neue Mitgliedschaft (-00) mit dem Start-Datum=Todesdatum+1 anlegen | ||||
|  | ||||
|         // die Geschäftsanteile per share-tx: TRANSFER→ADOPT an die Erbengemeinschaft übertragen | ||||
|         // die Geschäftsguthaben per asset-tx: TRANSFER→ADOPT an die Erbengemeinschaft übertragen | ||||
|         obtain( | ||||
|                 "Representative-Relation: %{representativeGivenName} %{representativeFamilyName} for Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", | ||||
|                 () -> httpPost("/api/hs/office/relations", | ||||
|                         usingJsonBody(""" | ||||
|                                 { | ||||
|                                    "type": "REPRESENTATIVE", | ||||
|                                    "anchor.uuid": ${Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}, | ||||
|                                    "holder": { | ||||
|                                         "personType": "NATURAL_PERSON", | ||||
|                                         "givenName": ${representativeGivenName}, | ||||
|                                         "familyName": ${representativeFamilyName} | ||||
|                                     }, | ||||
|                                    "contact.uuid": ${Contact: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}} | ||||
|                                 } | ||||
|                                 """)) | ||||
|                         .reportWithResponse().expecting(CREATED).expecting(JSON) | ||||
|         ) | ||||
|                 .extractUuidAlias("holder.uuid", "Person: %{representativeGivenName} %{representativeFamilyName}"); | ||||
|  | ||||
|         // outro: die Erbengemeinschaft hat eine Frist von 6 Monaten, um die Mitgliedschaft einer Person zu übertragen | ||||
|         // →nächster "Drecksfall" | ||||
| @@ -120,20 +100,44 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase<ReplaceD | ||||
|                 "Verify the Updated Partner", | ||||
|                 () -> httpGet("/api/hs/office/partners/%{partnerNumber}") | ||||
|                         .expecting(OK).expecting(JSON).expectObject(), | ||||
|                 path("partnerRel.holder.tradeName").contains("Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}") | ||||
|                 path("partnerRel.holder.tradeName").contains( | ||||
|                         "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"), | ||||
|                 path("partnerRel.contact.postalAddress").lenientlyContainsJson("§{communityOfHeirsPostalAddress}"), | ||||
|                 path("partnerRel.contact.phoneNumbers.office").contains("%{communityOfHeirsOfficePhoneNumber}"), | ||||
|                 path("partnerRel.contact.emailAddresses.main").contains("%{communityOfHeirsEmailAddress}") | ||||
|         ); | ||||
|  | ||||
|         // TODO.test: Verify the EX_PARTNER-Relation, once we fixed the anchor problem, see HsOfficePartnerController | ||||
|         // (net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerController.optionallyCreateExPartnerRelation) | ||||
|         verify( | ||||
|                 "Verify the Ex-Partner-Relation", | ||||
|                 () -> httpGet( | ||||
|                         "/api/hs/office/relations?relationType=EX_PARTNER&personUuid=%{Person: %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}") | ||||
|                         .expecting(OK).expecting(JSON).expectArrayElements(1), | ||||
|                 path("[0].anchor.tradeName").contains( | ||||
|                         "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}") | ||||
|         ); | ||||
|  | ||||
|         verify( | ||||
|                 "Verify the Representative-Relation", | ||||
|                 () -> httpGet("/api/hs/office/relations?relationType=REPRESENTATIVE&personUuid=%{Person: %{representativeGivenName} %{representativeFamilyName}}") | ||||
|                 () -> httpGet( | ||||
|                         "/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].anchor.tradeName").contains( | ||||
|                         "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"), | ||||
|                 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}") | ||||
|         ); | ||||
|  | ||||
|         // TODO.test: Verify Debitor, Membership, Coop-Shares and Coop-Assets once implemented | ||||
|         verify( | ||||
|                 "Verify the Debitor-Relation", | ||||
|                 () -> httpGet( | ||||
|                         "/api/hs/office/debitors?partnerNumber=%{partnerNumber}") | ||||
|                         .expecting(OK).expecting(JSON).expectArrayElements(1), | ||||
|                 path("[0].debitorRel.anchor.tradeName").contains( | ||||
|                         "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"), | ||||
|                 path("[0].debitorRel.holder.tradeName").contains( | ||||
|                         "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}") | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<UseCase.HttpResponse> 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<HttpResponse> doesNotExist() { | ||||
|         return response -> { | ||||
|             try { | ||||
|   | ||||
| @@ -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"); | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -19,7 +19,7 @@ import static org.assertj.core.api.Assumptions.assumeThat; | ||||
| public abstract class PatchUnitTestBase<R, E> { | ||||
|  | ||||
|     @Test | ||||
|     void willPatchNoProperty() { | ||||
|     protected void willPatchNoProperty() { | ||||
|         // given | ||||
|         final var givenEntity = newInitialEntity(); | ||||
|         final var patchResource = newPatchResource(); | ||||
| @@ -73,7 +73,7 @@ public abstract class PatchUnitTestBase<R, E> { | ||||
|  | ||||
|     @ParameterizedTest | ||||
|     @MethodSource("propertyTestCases") | ||||
|     void willThrowExceptionIfNotNullableValueIsNull(final Property<R, Object, E, Object> testCase) { | ||||
|     protected void willThrowExceptionIfNotNullableValueIsNull(final Property<R, Object, E, Object> testCase) { | ||||
|         assumeThat(testCase instanceof JsonNullableProperty).isTrue(); | ||||
|         assumeThat(testCase.nullable).isFalse(); | ||||
|  | ||||
| @@ -94,7 +94,7 @@ public abstract class PatchUnitTestBase<R, E> { | ||||
|  | ||||
|     @ParameterizedTest | ||||
|     @MethodSource("propertyTestCases") | ||||
|     void willPatchOnlyGivenPropertyToNull(final Property<R, Object, E, Object> testCase) { | ||||
|     protected void willPatchOnlyGivenPropertyToNull(final Property<R, Object, E, Object> testCase) { | ||||
|         assumeThat(testCase.nullable).isTrue(); | ||||
|  | ||||
|         // given | ||||
| @@ -113,7 +113,7 @@ public abstract class PatchUnitTestBase<R, E> { | ||||
|  | ||||
|     @ParameterizedTest | ||||
|     @MethodSource("propertyTestCases") | ||||
|     void willNotPatchIfGivenPropertyNotGiven(final Property<R, Object, E, Object> testCase) { | ||||
|     protected void willNotPatchIfGivenPropertyNotGiven(final Property<R, Object, E, Object> testCase) { | ||||
|  | ||||
|         // given | ||||
|         final var givenEntity = newInitialEntity(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user