amend API to patch partner person directly into partner

This commit is contained in:
Michael Hoennig 2025-03-04 17:16:27 +01:00
parent 839703ca48
commit f645b97533
10 changed files with 166 additions and 78 deletions

View File

@ -157,32 +157,33 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
context.define(currentSubject, assumedRoles);
final var current = rbacPartnerRepo.findByUuid(partnerUuid).orElseThrow();
final var previousPartnerRel = current.getPartnerRel();
final var previousPartnerPerson = current.getPartnerRel().getHolder();
new HsOfficePartnerEntityPatcher(em, current).apply(body);
final var saved = rbacPartnerRepo.save(current);
optionallyCreateExPartnerRelation(saved, previousPartnerRel);
optionallyUpdateRelatedRelations(saved, previousPartnerRel);
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) {
private void optionallyCreateExPartnerRelation(final HsOfficePartnerRbacEntity saved, final HsOfficePersonRealEntity previousPartnerPerson) {
final var partnerRelHasChanged = !saved.getPartnerRel().getUuid().equals(previousPartnerRel.getUuid());
if (partnerRelHasChanged) {
realRelationRepo.save(previousPartnerRel.toBuilder().uuid(null)
.type(EX_PARTNER).anchor(saved.getPartnerRel().getHolder())
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 HsOfficeRelationRealEntity previousPartnerRel) {
final var oldPartnerPerson = previousPartnerRel.getHolder();
final var newPartnerPerson = saved.getPartnerRel().getHolder();
final var partnerPersonHasChanged = !newPartnerPerson.getUuid().equals(oldPartnerPerson.getUuid());
private void optionallyUpdateRelatedRelations(final HsOfficePartnerRbacEntity saved, final HsOfficePersonRealEntity previousPartnerPerson) {
final var partnerPersonHasChanged = !saved.getPartnerRel().getHolder().getUuid().equals(previousPartnerPerson.getUuid());
if (partnerPersonHasChanged) {
}

View File

@ -1,9 +1,8 @@
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.HsOfficeRelationEntityPatcher;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import jakarta.persistence.EntityManager;
@ -19,17 +18,13 @@ class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatch
@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 HsOfficeRelationEntityPatcher(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());
}
}
}

View File

@ -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);

View File

@ -0,0 +1,34 @@
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.HsOfficeRelationContactPatchResource;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import jakarta.persistence.EntityManager;
import java.util.UUID;
public class HsOfficeRelationEntityContactPatcher implements EntityPatcher<HsOfficeRelationContactPatchResource> {
private final EntityManager em;
private final HsOfficeRelation entity;
public HsOfficeRelationEntityContactPatcher(final EntityManager em, final HsOfficeRelation entity) {
this.em = em;
this.entity = entity;
}
@Override
public void apply(final HsOfficeRelationContactPatchResource resource) {
OptionalFromJson.of(resource.getContactUuid()).ifPresent(newValue -> {
verifyNotNull(newValue, "contact");
entity.setContact(em.getReference(HsOfficeContactRealEntity.class, newValue));
});
}
private void verifyNotNull(final UUID newValue, final String propertyName) {
if (newValue == null) {
throw new IllegalArgumentException("property '" + propertyName + "' must not be null");
}
}
}

View File

@ -2,24 +2,33 @@ 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.OptionalFromJson;
import jakarta.persistence.EntityManager;
import java.util.UUID;
class HsOfficeRelationEntityPatcher implements EntityPatcher<HsOfficeRelationPatchResource> {
public class HsOfficeRelationEntityPatcher implements EntityPatcher<HsOfficeRelationPatchResource> {
private final EntityManager em;
private final HsOfficeRelation entity;
HsOfficeRelationEntityPatcher(final EntityManager em, final HsOfficeRelation entity) {
public HsOfficeRelationEntityPatcher(final EntityManager em, final HsOfficeRelation entity) {
this.em = em;
this.entity = entity;
}
@Override
public void apply(final HsOfficeRelationPatchResource resource) {
OptionalFromJson.of(resource.getAnchorUuid()).ifPresent(newValue -> {
verifyNotNull(newValue, "contact");
entity.setAnchor(em.getReference(HsOfficePersonRealEntity.class, newValue));
});
OptionalFromJson.of(resource.getHolderUuid()).ifPresent(newValue -> {
verifyNotNull(newValue, "contact");
entity.setHolder(em.getReference(HsOfficePersonRealEntity.class, newValue));
});
OptionalFromJson.of(resource.getContactUuid()).ifPresent(newValue -> {
verifyNotNull(newValue, "contact");
entity.setContact(em.getReference(HsOfficeContactRealEntity.class, newValue));

View File

@ -48,10 +48,8 @@ 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'

View File

@ -34,9 +34,25 @@ components:
contact:
$ref: 'hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
HsOfficeRelationContactPatch:
type: object
properties:
contact.uuid:
type: string
format: uuid
nullable: true
HsOfficeRelationPatch:
type: object
properties:
anchor.uuid:
type: string
format: uuid
nullable: true
holder.uuid:
type: string
format: uuid
nullable: true
contact.uuid:
type: string
format: uuid

View File

@ -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

View File

@ -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,8 +409,8 @@ 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;
});

View File

@ -1,20 +1,24 @@
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 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 +26,17 @@ import static org.mockito.Mockito.lenient;
@TestInstance(PER_CLASS)
@ExtendWith(MockitoExtension.class)
class HsOfficePartnerEntityPatcherUnitTest extends PatchUnitTestBase<
HsOfficePartnerPatchResource,
HsOfficePartnerRbacEntity
> {
// This test class does not rerive from 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 +45,72 @@ class HsOfficePartnerEntityPatcherUnitTest extends PatchUnitTestBase<
private final HsOfficePartnerDetailsEntity givenInitialDetails = HsOfficePartnerDetailsEntity.builder()
.uuid(INITIAL_DETAILS_UUID)
.build();
@Mock
private EntityManager em;
@BeforeEach
void initMocks() {
lenient().when(em.getReference(eq(HsOfficeRelationRealEntity.class), any())).thenAnswer(invocation ->
HsOfficeRelationRealEntity.builder().uuid(invocation.getArgument(1)).build());
lenient().when(em.getReference(eq(HsOfficePersonRealEntity.class), any())).thenAnswer(invocation ->
HsOfficePersonRealEntity.builder().uuid(invocation.getArgument(1)).build());
lenient().when(em.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 +118,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();
}
}