From 62dbe0fd4a7ffe6779dc9e87c0143288281e63ed Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 26 Feb 2025 09:13:00 +0100 Subject: [PATCH 01/13] updatable relation anchor and holder --- .../relation/HsOfficeRelationRbacEntity.java | 2 +- .../5033-hs-office-relation-rbac.sql | 20 ++++++++++++++++++- ...ficeRelationRepositoryIntegrationTest.java | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacEntity.java index c0f8476f..35063799 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacEntity.java @@ -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") .importEntityAlias("anchorPerson", HsOfficePersonRbacEntity.class, usingDefaultCase(), dependsOnColumn("anchorUuid"), directlyFetchedByDependsOnColumn(), diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql index 260391bf..97e5bed8 100644 --- a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql @@ -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$); --// @@ -305,3 +309,17 @@ END; $$; --// + +-- ============================================================================ +--changeset RbacRbacSystemRebuildGenerator:hs-office-relation-rbac-actually-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--// +-- ---------------------------------------------------------------------------- + +begin transaction; + call base.defineContext( + 're-creating RBAC for table hs_office.relation', + null, + 'superuser-alex@hostsharing.net' -- FIXME: use env-var + ); + call hs_office.relation_rebuild_rbac_system(); +commit; +--// diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java index 5e8c750b..4a9a1cee 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java @@ -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; -- 2.39.5 From 982b4d9c90193266a5c3d444ed03620579ae8713 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 26 Feb 2025 11:50:47 +0100 Subject: [PATCH 02/13] use the new partner-person for ex-partner-relation --- .../hs/office/partner/HsOfficePartnerController.java | 5 +++-- .../partner/HsOfficePartnerControllerAcceptanceTest.java | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index aa8b572e..da0bbd37 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -170,8 +170,9 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { 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()); + relationRepo.save(previousPartnerRel.toBuilder().uuid(null) + .type(EX_PARTNER).anchor(saved.getPartnerRel().getHolder()) + .build()); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java index 7afd9714..4eac16e8 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java @@ -411,10 +411,10 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu }); // 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 -- 2.39.5 From 06aca56c5cc676a503cbac0d2e011e49149a5929 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 2 Mar 2025 17:00:18 +0100 Subject: [PATCH 03/13] ADR for exchanging the partner (+debitor) person --- ...025-02-27-exchanging-the-partner-person.md | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 doc/adr/2025-02-27-exchanging-the-partner-person.md diff --git a/doc/adr/2025-02-27-exchanging-the-partner-person.md b/doc/adr/2025-02-27-exchanging-the-partner-person.md new file mode 100644 index 00000000..1d2dedd3 --- /dev/null +++ b/doc/adr/2025-02-27-exchanging-the-partner-person.md @@ -0,0 +1,108 @@ +# Änderung eines Geschäftspartners oder Rechnungsempfängers (Debitor) + +**Status:** +- [x] vorgeschlagen von (Michael Hönnig) +- [ ] akzeptiert von (...) +- [ ] abgelehnt von (...) +- [ ] ersetzt durch (ersetzende ADR) + +## Kontext und Problemstellung + +In vorgegebenen Datenmodell von Geschäftspartnern und Rechnungsempfängern (Debitor), das auch fachliche Rollen wie Repräsentant, technische Ansprechpartner oder modellieren kann, stellt sich die Frage, wie eine Änderung der Geschäftspartner-Person effizient und konsistent umgesetzt werden kann. +Diese fachlichen Rollen hängen jeweils an der Partner-Person. + +Ein konkretes Beispiel hierfür ist die Änderung von einer natürlichen Person, die verstorben ist, zu deren Erbengemeinschaft. +**Hierbei stellte sich heraus, dass der die API-Bedienung sehr komplex und damit fehleranfällig ist, weil viele neue Objekte erzeugt und korrekt miteinander verbunden werden müssen. Dies wäre zudem nicht transaktionssicher.** + +Angepasst werden müssen: + +1. alle Relations mit der alten Partner-Person: +- die PARTNER-Relation +- die DEBITOR-Relations (ggf. mehrere) +- die OPERATIONS-Relations (ggf. mehrere) +- die SUBSCRIBER-Relations (ggf. mehrere) +- die REPRESENTATIVE-Relations (ggf. mehrere) +- etc. +2. Die PARTNER-Relation hat die Besonderheit, dass sie zusätzlich im Debitor ausgetauscht werden muss. +3. Die DEBITOR-Relation die Besonderheit, dass sie zusätzlich im Debitor ausgetauscht werden muss. + +Daher sollen möglichst viele dieser *Neuverdrahtungen* im Backend gemacht werden. +Und dafür braucht es dann eine zentrale Stelle, an der die Kaskade ausgelöst wird. + +Derzeit gibt es zwei mögliche Varianten, diese Änderung dynamisch umzusetzen, die jeweils unterschiedliche Auswirkungen auf die API und die Zugriffsrechte haben. + +### Technischer Hintergrund + +Zum Zeitpunkt der Erstellung dieses ADR existieren folgende relevante Entitäten: +- **Person**: Natürliche oder juristische Person (Name, Firma, Anrede etc.) +- **Contact**: Kontaktdaten einer fachlichen Rolle +- **Relation**: Mit einem Typ (z.B. PARTNER, DEBITOR, REPRESENTATIVE) und Kontaktdaten versehene Beziehung von einer Person (Holder) zu einer anderen (Anchor) +- **Partner**: Sind quasi Zusatzdaten einer PARTNER-Relation (derzeit nur die Partnernummer), welche eine Partner-Person mit der Hostsharing-Person verknüpft +- **Debitor**: Sind quasi Zusatzdaten einer DEBITOR-Relation, welche eine Debitor-Person mit einer Partner-Person verknüpft + +Zugriffsrechte werden über ein hierarchisches, dynamisches RBAC-System gesteuert, bei dem der **OWNER** einer Entitäten-Instanz alle Rechte hat, **ADMIN** definierte Spalten aktualisieren darf, **AGENT** Verknüpfungen anlegen kann, und **TENANT**, **GUEST** sowie **REFERRER** nur Lesezugriff haben. +Partner und Debitor nutzen dabei die RBAC-Rollen der zugehörigen Relations. + +## In Betracht gezogene Optionen + +* **Variante 1:** Austausch der PARTNER-/DEBITOR-/OPERATIONS-/...-Relations gegen eine neue Relation für die Erbengemeinschaft als neuen Holder +* **Variante 2:** Änderung des Holders in der bestehenden PARTNER-Relation auf die Erbengemeinschaft + +### Variante 1: Austausch der Relations mit neuen Holdern + +Ein Austausch der bestehenden PARTNER-/DEBITOR-/OPERATIONS-/...-Relations mit einer neuen Relation, die die Erbengemeinschaft als neuen Holder referenziert. + +#### Vorteile + +- **Beibehaltung der API:** Dieses Verhalten ist bereits implementiert und benötigt keinen großen Umbau an der API, sondern nur eine Erweiterung um das Austauschen weiterer Relations +- **UPDATE-Permission für AGENT:** Es wäre möglich, der AGENT-Rolle einer Relation UPDATE-Rechte an der Relation zu geben, weil nur der unkritisch Contact änderbar wäre. +- **Übereinstimmung von Fachlichkeit und API**: Fachlich handelt es sich um den Austausch der Partner-Person, dazu passend wäre der Endpunkt, allerdings würde nicht direkt die Partner-Person ausgetauscht, sondern eine neue PARTNER-Relation mit der neuen Partner-Person eingesetzt werden. + +#### Nachteile + +- **Verlust expliziter GRANTs:** Gibt es explizite GRANTs an der PARTNER-Relation, gehen diese verloren, da die Relation ausgetauscht wird. Die Übernahme dieser expliziten Grants erfordert also einen zusätzlichen Implementationsaufwand. +- **Divergenz zwischen Fachlichkeit und API:** Fachlich handelt es sich um den Austausch der Partner-Person, würde aber eine neue PARTNER-Relation dieser Person in den Partner eingesetzt werden. Das erfordert ein höheres Verständnis des Datenmodells. + +### Variante 2: Änderung des Holders in der bestehenden PARTNER-Relation + +Die bestehende PARTNER-Relation bleibt erhalten, und der Holder wird von der verstorbenen Person auf die Erbengemeinschaft geändert. + +#### Vorteile + +- **Erhalt expliziter GRANTs:** Wer explizite Grants an der PARTNER-Relation oder DEBITOR-Relation vergeben hat, behält diese, da die Relation-Instanzen unverändert bleiben. +- **Einheitliche API-Struktur:** Die REST-API für Änderungen gehört dann einheitlich zum Relation-Endpunkt, was der bestehenden Handhabung von Contact-Änderungen entspricht. +- **Übereinstimmung von Fachlichkeit und API**: Fachlich handelt es sich um den Austausch der Partner-Person, genau das würde man dann an der API machen, wenn auch nicht am Partner selbst, sondern an der PARTNER-Relation. + +#### Nachteile + +- **Kein UPDATE durch Relation-AGENT:** Der Relation-AGENT darf nicht das Recht bekommen, den Holder auszutauschen. Da es keine Spalten-spezifischen Update-Rechte gibt, könnte dieser auch den Contact nicht mehr austauschen. Derzeit ist das aber auch nicht vorgesehen. +- **Umbau der API:** Der Austausch einer Partner-Person würde vom Partner-Endpunkt (/api/hs/office/partner) zur Relation (/api/hs/office/partner) wandern, was ein größerer Umbau, auch bei den Tests wäre. +- **Divergenz von Fachlichkeit und API**: Fachlich handelt es sich um den Austausch der Partner-Person, aber man würde die Person nicht am Partner selbst austauschen, sondern an der PARTNER-Relation. + +## Entscheidung und Ergebnis + +**Entscheidung:** Noch kein klares Ergebnis + +**Begründung:** +- Die meisten Vor- und Nachteile gleichen sich aus, was besonders bei der Übereinstimmung bzw. Divergenz zwischen Fachlichkeit und API zum Ausdruck kommt. +- Diese Variante erfordert keinen grundsätzlichen Umbau der API und daher weniger aufwändig. +- Ein großer Aufwand, nämlich die Übernahme der GRANTs, könnte sogar zunächst zurückgestellt werden. + +| Bereich | 1. Relations ersetzen | 2. Relations aktualisieren | +|------------------------------------------------------------|----------------------:|---------------------------:| +| **Aufwände** | | | +| Beibehaltung der API vs. Umbau, inkl. Risiko | | -3 | +| Anwendbar auf Partner-Person + Debitor-Person | | +1 | +| Aufwand für explizite Grants | -1 | | +| **Zwischenergebnis für Aufwände** | **-1** | **-2** | +| | | | +| **Fachlichkeit/Einheitlichkeit etc.** | | | +| Kongruenz von Fachlichkeit+API | +1 | -1 | +| Einheitlichkeit/Generizität der API | | +1 | +| Direktheit der API | | +1 | +| UPDATE Permission für Relation-AGENT möglich | +1 | | +| **Zwischenergebnis für Fachlichkeit/Einheitlichkeit etc.** | **+2** | **+1** | +| | | | +| **Ergebnis** | **+1** | **-1** | + + -- 2.39.5 From 4923f02de602c2cecddc6ba8e52146030b959bde Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 3 Mar 2025 08:51:40 +0100 Subject: [PATCH 04/13] optionallyUpdateRelatedRelations - WIP --- .../hs/office/partner/HsOfficePartnerController.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index da0bbd37..d2403078 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -163,6 +163,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { final var saved = partnerRepo.save(current); optionallyCreateExPartnerRelation(saved, previousPartnerRel); + optionallyUpdateRelatedRelations(saved, previousPartnerRel); final var mapped = mapper.map(saved, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(mapped); @@ -176,6 +177,14 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { } } + private void optionallyUpdateRelatedRelations(final HsOfficePartnerRbacEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) { +// if (!saved.getPartnerRel().getUuid().equals(previousPartnerRel.getUuid())) { +// relationRepo.save(previousPartnerRel.toBuilder().uuid(null) +// .type(EX_PARTNER).anchor(saved.getPartnerRel().getHolder()) +// .build()); +// } + } + private HsOfficePartnerRbacEntity createPartnerEntity(final HsOfficePartnerInsertResource body) { final var entityToSave = new HsOfficePartnerRbacEntity(); entityToSave.setPartnerNumber(cropTag(HsOfficePartnerRbacEntity.PARTNER_NUMBER_TAG, body.getPartnerNumber())); -- 2.39.5 From af374ed220c6f2ea38450cb1a8239740fa7b6ffe Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 4 Mar 2025 10:55:04 +0100 Subject: [PATCH 05/13] add 3rd variant to ADR: updating the Partner-Relation but via Partner-API --- ...025-02-27-exchanging-the-partner-person.md | 98 +++++++++++-------- 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/doc/adr/2025-02-27-exchanging-the-partner-person.md b/doc/adr/2025-02-27-exchanging-the-partner-person.md index 1d2dedd3..d9adc200 100644 --- a/doc/adr/2025-02-27-exchanging-the-partner-person.md +++ b/doc/adr/2025-02-27-exchanging-the-partner-person.md @@ -8,11 +8,11 @@ ## Kontext und Problemstellung -In vorgegebenen Datenmodell von Geschäftspartnern und Rechnungsempfängern (Debitor), das auch fachliche Rollen wie Repräsentant, technische Ansprechpartner oder modellieren kann, stellt sich die Frage, wie eine Änderung der Geschäftspartner-Person effizient und konsistent umgesetzt werden kann. +Im vorgegebenen Datenmodell von Geschäftspartnern und Rechnungsempfängern (Debitoren), das auch fachliche Rollen wie Repräsentant, technische Ansprechpartner oder Mailinglisten-Subscriptions umfasst, stellt sich die Frage, wie eine Änderung der Geschäftspartner-Person effizient und konsistent umgesetzt werden kann. Diese fachlichen Rollen hängen jeweils an der Partner-Person. Ein konkretes Beispiel hierfür ist die Änderung von einer natürlichen Person, die verstorben ist, zu deren Erbengemeinschaft. -**Hierbei stellte sich heraus, dass der die API-Bedienung sehr komplex und damit fehleranfällig ist, weil viele neue Objekte erzeugt und korrekt miteinander verbunden werden müssen. Dies wäre zudem nicht transaktionssicher.** +**Hierbei zeigte sich, dass die API-Bedienung durch die Vielzahl neu zu erstellender Objekte und deren Verknüpfungen komplex und fehleranfällig ist. Zudem lassen sich nicht alle Änderung in einer einzigen Transaktion durchführen, was zu Inkonsistenzen führen kann.“** Angepasst werden müssen: @@ -23,13 +23,13 @@ Angepasst werden müssen: - die SUBSCRIBER-Relations (ggf. mehrere) - die REPRESENTATIVE-Relations (ggf. mehrere) - etc. -2. Die PARTNER-Relation hat die Besonderheit, dass sie zusätzlich im Debitor ausgetauscht werden muss. -3. Die DEBITOR-Relation die Besonderheit, dass sie zusätzlich im Debitor ausgetauscht werden muss. +2. Die PARTNER-Relation hat die Besonderheit, dass sie vom Partner referenziert wird und daher auch dort ausgetauscht werden muss. +3. Die DEBITOR-Relation hat die Besonderheit, dass sie vom Debitor referenziert wird und daher auch dort ausgetauscht werden muss. Daher sollen möglichst viele dieser *Neuverdrahtungen* im Backend gemacht werden. Und dafür braucht es dann eine zentrale Stelle, an der die Kaskade ausgelöst wird. -Derzeit gibt es zwei mögliche Varianten, diese Änderung dynamisch umzusetzen, die jeweils unterschiedliche Auswirkungen auf die API und die Zugriffsrechte haben. +Derzeit gibt es drei mögliche Varianten, diese Änderung dynamisch umzusetzen, die jeweils unterschiedliche Auswirkungen auf Aufwände, API und Zugriffsrechte haben. ### Technischer Hintergrund @@ -43,66 +43,82 @@ Zum Zeitpunkt der Erstellung dieses ADR existieren folgende relevante Entitäten Zugriffsrechte werden über ein hierarchisches, dynamisches RBAC-System gesteuert, bei dem der **OWNER** einer Entitäten-Instanz alle Rechte hat, **ADMIN** definierte Spalten aktualisieren darf, **AGENT** Verknüpfungen anlegen kann, und **TENANT**, **GUEST** sowie **REFERRER** nur Lesezugriff haben. Partner und Debitor nutzen dabei die RBAC-Rollen der zugehörigen Relations. -## In Betracht gezogene Optionen +## In Betracht gezogene Varianten -* **Variante 1:** Austausch der PARTNER-/DEBITOR-/OPERATIONS-/...-Relations gegen eine neue Relation für die Erbengemeinschaft als neuen Holder -* **Variante 2:** Änderung des Holders in der bestehenden PARTNER-Relation auf die Erbengemeinschaft +* **1. Relations ersetzen:** Austausch der PARTNER-/DEBITOR-/OPERATIONS-/...-Relations gegen eine neue Relation für die neue Partner-Person (z.B. Erbengemeinschaft) als neuen Holder als PATCH auf /api/hs/office/partners/UUID +* **2. Relations direkt aktualisieren:** Änderung der Holder-Referenz in der bestehenden PARTNER-Relation auf die neue Partner-Person (z.B. Erbengemeinschaft) als PATCH auf /api/hs/office/relations/UUID +* **3. Relations via Partner aktualisieren:** Änderung der Partner-Person in die PARTNER-Relation als PATCH auf /api/hs/office/partners/UUID -### Variante 1: Austausch der Relations mit neuen Holdern +### Variante 1: Relations ersetzen -Ein Austausch der bestehenden PARTNER-/DEBITOR-/OPERATIONS-/...-Relations mit einer neuen Relation, die die Erbengemeinschaft als neuen Holder referenziert. +Der Austausch der Partner- (und Debitor-) Person erfolgt über das Erstellen einer neuen PARTNER- bzw. DEBITOR-Relation, im Partner bzw. Debitor wird dann die Referenz auf die alte PARTNER- bzw. DEBITOR-Relation gegen die neue ausgetauscht. #### Vorteile -- **Beibehaltung der API:** Dieses Verhalten ist bereits implementiert und benötigt keinen großen Umbau an der API, sondern nur eine Erweiterung um das Austauschen weiterer Relations -- **UPDATE-Permission für AGENT:** Es wäre möglich, der AGENT-Rolle einer Relation UPDATE-Rechte an der Relation zu geben, weil nur der unkritisch Contact änderbar wäre. -- **Übereinstimmung von Fachlichkeit und API**: Fachlich handelt es sich um den Austausch der Partner-Person, dazu passend wäre der Endpunkt, allerdings würde nicht direkt die Partner-Person ausgetauscht, sondern eine neue PARTNER-Relation mit der neuen Partner-Person eingesetzt werden. +- **Beibehaltung der API:** Dieses Verhalten ist bereits implementiert und benötigt keinen großen Umbau an der API, sondern nur eine Erweiterung um das Austauschen weiterer Relations. +- **UPDATE-Permission für AGENT:** Es wäre möglich, der AGENT-Rolle einer Relation UPDATE-Rechte an der Relation zu geben, weil nur die unkritische Contact-Referenz änderbar wäre. +- **Kongruenz von Fachlichkeit+API**: Fachlich handelt es sich um den Austausch der Partner-Person, dazu passend wäre der Endpunkt, allerdings wird in dieser Variante nicht direkt die Partner-Person ausgetauscht, sondern eine neue PARTNER-Relation mit der neuen Partner-Person eingesetzt. #### Nachteile - **Verlust expliziter GRANTs:** Gibt es explizite GRANTs an der PARTNER-Relation, gehen diese verloren, da die Relation ausgetauscht wird. Die Übernahme dieser expliziten Grants erfordert also einen zusätzlichen Implementationsaufwand. - **Divergenz zwischen Fachlichkeit und API:** Fachlich handelt es sich um den Austausch der Partner-Person, würde aber eine neue PARTNER-Relation dieser Person in den Partner eingesetzt werden. Das erfordert ein höheres Verständnis des Datenmodells. +- **Keine Anwendbarkeit auf abhängige Relations:** Beim Aktualisieren der abhängigen Relations (z.B. Representative, Operational- und Billing-Kontakt sowie der Mailinglisten-Subscriptions) stehen wir wieder vor dem Ausgangsproblem und müssten jeweils neue Relations erzeugen und die alten Relations löschen, was dann wieder zum Verlust expliziter GRANTs führt. +- **Performance bei vielen abhängigen Relations:** die abhängigen Relations können nur über Loops, nicht aber durch direkt SQL UPDATEs ausgetauscht werden, was zu einer schlechteren Performance führt -### Variante 2: Änderung des Holders in der bestehenden PARTNER-Relation +### Variante 2: Relations direkt aktualisieren -Die bestehende PARTNER-Relation bleibt erhalten, und der Holder wird von der verstorbenen Person auf die Erbengemeinschaft geändert. +Die bestehende PARTNER-Relation bliebe erhalten, und der Holder wird von der verstorbenen Person auf die Erbengemeinschaft geändert. #### Vorteile -- **Erhalt expliziter GRANTs:** Wer explizite Grants an der PARTNER-Relation oder DEBITOR-Relation vergeben hat, behält diese, da die Relation-Instanzen unverändert bleiben. -- **Einheitliche API-Struktur:** Die REST-API für Änderungen gehört dann einheitlich zum Relation-Endpunkt, was der bestehenden Handhabung von Contact-Änderungen entspricht. -- **Übereinstimmung von Fachlichkeit und API**: Fachlich handelt es sich um den Austausch der Partner-Person, genau das würde man dann an der API machen, wenn auch nicht am Partner selbst, sondern an der PARTNER-Relation. +- **Anwendbarkeit auf Partner- und Debitor-Person:** Der Code wäre an einer generischen Stelle, welche dann Partner- und Debitor-Person austauschbar machen würde +- **Einheitlichkeit/Generizität der API:** Die REST-API für Änderungen gehört dann einheitlich zum Relation-Endpunkt, was der bestehenden Handhabung von Contact-Änderungen entspricht. #### Nachteile -- **Kein UPDATE durch Relation-AGENT:** Der Relation-AGENT darf nicht das Recht bekommen, den Holder auszutauschen. Da es keine Spalten-spezifischen Update-Rechte gibt, könnte dieser auch den Contact nicht mehr austauschen. Derzeit ist das aber auch nicht vorgesehen. +- **UPDATE Permission für Relation-AGENT wäre kritisch:** Der Relation-AGENT darf nicht das Recht bekommen, den Holder auszutauschen. Da es keine Spalten-spezifischen Update-Rechte gibt, könnte dieser auch den Contact nicht mehr austauschen. Derzeit ist das allerdings auch noch nicht so implementiert. - **Umbau der API:** Der Austausch einer Partner-Person würde vom Partner-Endpunkt (/api/hs/office/partner) zur Relation (/api/hs/office/partner) wandern, was ein größerer Umbau, auch bei den Tests wäre. - **Divergenz von Fachlichkeit und API**: Fachlich handelt es sich um den Austausch der Partner-Person, aber man würde die Person nicht am Partner selbst austauschen, sondern an der PARTNER-Relation. +### Variante 3: Relations via Partner aktualisieren + +Der Austausch der Partner- (bzw. Debitor-) Person würde weiterhin beim Partner bzw. Debitor erfolgen, jedoch würde die Personen-Referenz direkt in der bestehenden Partner- (bzw. Debitor-) Relation umgesetzt werden, statt eine neue Relation mit der neuen Partner- (bzw. Debitor) Person einzusetzen. Die direkt wie auch abhängige Relations könnten also einfach per SQL UPDATE aktualisiert werden. + +#### Vorteile + +- **Beibehaltung der API:** Der Endpunkt /api/hs/office/partners/UUID bliebe erhalten, wenn auch lokal ein Umbau auf Person-Update statt Relation-Update erfolgen müsste, Anpassungen in Verwendungen dieser API, z.B. in Tests, wären allerdings wenig aufwändig und das Risiko für weitere Aufwände recht gering. +- **UPDATE-Permission für AGENT:** Es wäre möglich, der AGENT-Rolle einer Relation UPDATE-Rechte an der Relation zu geben, aber eine Aktualisierung über die REST-Controller nur an kontrollierten Stellen zuzulassen. +- **Kongruenz von Fachlichkeit+API**: Fachlich handelt es sich um den Austausch der Partner-Person, was auch in dieser Variante technisch abgebildet würde, wenn auch eine Ebene tiefer im JSON, nämlich in der Partner-Relation. + +#### Nachteile + +Nennenswerte Nachteile wurden nicht identifiziert, allenfalls ist es etwas schräge, dass die RBAC-Rechte an den Relations ein UPDATE zulassen, was aber an der API nur für bestimmte Relations (ggf. kontrolliert) erreichbar wäre. + + ## Entscheidung und Ergebnis -**Entscheidung:** Noch kein klares Ergebnis +**Entscheidung:** 3. Relations via Partner aktualisieren **Begründung:** -- Die meisten Vor- und Nachteile gleichen sich aus, was besonders bei der Übereinstimmung bzw. Divergenz zwischen Fachlichkeit und API zum Ausdruck kommt. -- Diese Variante erfordert keinen grundsätzlichen Umbau der API und daher weniger aufwändig. -- Ein großer Aufwand, nämlich die Übernahme der GRANTs, könnte sogar zunächst zurückgestellt werden. - -| Bereich | 1. Relations ersetzen | 2. Relations aktualisieren | -|------------------------------------------------------------|----------------------:|---------------------------:| -| **Aufwände** | | | -| Beibehaltung der API vs. Umbau, inkl. Risiko | | -3 | -| Anwendbar auf Partner-Person + Debitor-Person | | +1 | -| Aufwand für explizite Grants | -1 | | -| **Zwischenergebnis für Aufwände** | **-1** | **-2** | -| | | | -| **Fachlichkeit/Einheitlichkeit etc.** | | | -| Kongruenz von Fachlichkeit+API | +1 | -1 | -| Einheitlichkeit/Generizität der API | | +1 | -| Direktheit der API | | +1 | -| UPDATE Permission für Relation-AGENT möglich | +1 | | -| **Zwischenergebnis für Fachlichkeit/Einheitlichkeit etc.** | **+2** | **+1** | -| | | | -| **Ergebnis** | **+1** | **-1** | - +- die Fachlichkeit wird an der API gut abgebildet (PATCH der Partner-Person auf /api/hs/office/partners/UUID) +- der Aufwand ist relativ gering (vieles ist mit SQL UPDATEs machbar) +- die UPDATE Permission dürfte an Relation-AGENT granted werden, ohne damit Schindluder getrieben werden kann (weil das an der API verhindert werden kann) +| Kriterium \ Relations ... | 1. ersetzen | 2. direkt aktualisieren | 3. via Partner aktualisieren | +|-----------------------------------------------|------------:|------------------------:|-----------------------------:| +| **Technische und Aufwands-Kriterien** | | | | +| Beibehaltung der API vs. Umbau (inkl. Risiko) | +2 | -2 | +1 | +| Anwendbarkeit auf Partner- und Debitor-Person | | +1 | | +| Anwendbarkeit auf abhängige Relations | -3 | | | +| Performance bei vielen abhängigen Relations | -1 | | | +| Aufwand für explizite Grants | -1 | | | +| **Zwischenergebnis** | **-3** | **-1** | **+1** | +| | | | | +| **Fachliche Kriterien.** | | | | +| Kongruenz von Fachlichkeit+API | +1 | -1 | +1 | +| Einheitlichkeit/Generizität der API | | +1 | | +| UPDATE Permission für Relation-AGENT möglich | +1 | | +1 | +| **Zwischenergebnis** | **+2** | **0** | **+2* | +| | | | | +| **Endergebnis** | **-1** | **-1** | **+3** | -- 2.39.5 From d4f97e9cf56002358f5c99dd6ed1693733284717 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 4 Mar 2025 11:14:39 +0100 Subject: [PATCH 06/13] English translation of ADR --- ...02-27-exchanging-the-partner-person.de.md} | 30 ++--- ...-02-27-exchanging-the-partner-person.en.md | 124 ++++++++++++++++++ 2 files changed, 139 insertions(+), 15 deletions(-) rename doc/adr/{2025-02-27-exchanging-the-partner-person.md => 2025-02-27-exchanging-the-partner-person.de.md} (84%) create mode 100644 doc/adr/2025-02-27-exchanging-the-partner-person.en.md diff --git a/doc/adr/2025-02-27-exchanging-the-partner-person.md b/doc/adr/2025-02-27-exchanging-the-partner-person.de.md similarity index 84% rename from doc/adr/2025-02-27-exchanging-the-partner-person.md rename to doc/adr/2025-02-27-exchanging-the-partner-person.de.md index d9adc200..e56aa28d 100644 --- a/doc/adr/2025-02-27-exchanging-the-partner-person.md +++ b/doc/adr/2025-02-27-exchanging-the-partner-person.de.md @@ -105,20 +105,20 @@ Nennenswerte Nachteile wurden nicht identifiziert, allenfalls ist es etwas schr - der Aufwand ist relativ gering (vieles ist mit SQL UPDATEs machbar) - die UPDATE Permission dürfte an Relation-AGENT granted werden, ohne damit Schindluder getrieben werden kann (weil das an der API verhindert werden kann) -| Kriterium \ Relations ... | 1. ersetzen | 2. direkt aktualisieren | 3. via Partner aktualisieren | -|-----------------------------------------------|------------:|------------------------:|-----------------------------:| -| **Technische und Aufwands-Kriterien** | | | | +| Kriterium \ Relations ... | 1. ersetzen | 2. direkt aktualisieren | 3. via Partner aktualisieren | +|----------------------------------------------|------------:|------------------------:|-----------------------------:| +| **Technische und Aufwands-Kriterien** | | | | | Beibehaltung der API vs. Umbau (inkl. Risiko) | +2 | -2 | +1 | | Anwendbarkeit auf Partner- und Debitor-Person | | +1 | | -| Anwendbarkeit auf abhängige Relations | -3 | | | -| Performance bei vielen abhängigen Relations | -1 | | | -| Aufwand für explizite Grants | -1 | | | -| **Zwischenergebnis** | **-3** | **-1** | **+1** | -| | | | | -| **Fachliche Kriterien.** | | | | -| Kongruenz von Fachlichkeit+API | +1 | -1 | +1 | -| Einheitlichkeit/Generizität der API | | +1 | | -| UPDATE Permission für Relation-AGENT möglich | +1 | | +1 | -| **Zwischenergebnis** | **+2** | **0** | **+2* | -| | | | | -| **Endergebnis** | **-1** | **-1** | **+3** | +| Anwendbarkeit auf abhängige Relations | -3 | | | +| Performance bei vielen abhängigen Relations | -1 | | | +| Aufwand für explizite Grants | -1 | | | +| **Zwischenergebnis** | **-3** | **-1** | **+1** | +| | | | | +| **Fachliche Kriterien** | | | | +| Kongruenz von Fachlichkeit+API | +1 | -1 | +1 | +| Einheitlichkeit/Generizität der API | | +1 | | +| UPDATE Permission für Relation-AGENT möglich | +1 | | +1 | +| **Zwischenergebnis** | **+2** | **0** | **+2* | +| | | | | +| **Endergebnis** | **-1** | **-1** | **+3** | diff --git a/doc/adr/2025-02-27-exchanging-the-partner-person.en.md b/doc/adr/2025-02-27-exchanging-the-partner-person.en.md new file mode 100644 index 00000000..79269b29 --- /dev/null +++ b/doc/adr/2025-02-27-exchanging-the-partner-person.en.md @@ -0,0 +1,124 @@ +# Changing a Business Partner or Invoice Recipient (Debitor) + +**Status:** +- [x] Proposed by (Michael Hönnig) +- [ ] Accepted by (...) +- [ ] Rejected by (...) +- [ ] Replaced by (replacing ADR) + +## Context and Problem Statement + +In the given data model of business partners and invoice recipients (debitors), which also includes business roles such as representative, technical contacts, or mailing list subscriptions, the question arises of how to efficiently and consistently implement a change of the business partner person. These business roles are each linked to the partner person. + +A concrete example is changing from a natural person who has passed away to their heir community. +**It has been shown that handling the API is complex and error-prone due to the large number of newly created objects and their links. Additionally, not all changes can be carried out in a single transaction, which can lead to inconsistencies.** + +The following elements must be updated: + +1. All relations with the old partner person: + - The PARTNER relation + - The DEBITOR relations (possibly multiple) + - The OPERATIONS relations (possibly multiple) + - The SUBSCRIBER relations (possibly multiple) + - The REPRESENTATIVE relations (possibly multiple) + - etc. +2. The PARTNER relation has the peculiarity that it is referenced by the partner and therefore must also be replaced there. +3. The DEBITOR relation has the peculiarity that it is referenced by the debitor and therefore must also be replaced there. + +As a result, as many of these *rewirings* as possible should be done in the backend. +A central point is needed to trigger this cascade. + +Currently, there are three possible approaches to implementing this change dynamically, each with different impacts on effort, API, and access rights. + +### Technical Background + +At the time of this ADR's creation, the following relevant entities exist: +- **Person**: A natural or legal entity (name, company, salutation, etc.) +- **Contact**: Contact data of a business role +- **Relation**: A relationship from one person (Holder) to another (Anchor), with a type (e.g., PARTNER, DEBITOR, REPRESENTATIVE) and contact data +- **Partner**: Essentially additional data of a PARTNER relation (currently only the partner number), linking a partner person to the Hostsharing person +- **Debitor**: Essentially additional data of a DEBITOR relation, linking a debitor person to a partner person + +Access rights are managed through a hierarchical, dynamic RBAC system, where the **OWNER** of an entity instance has all rights, **ADMIN** can update defined fields, **AGENT** can create links, and **TENANT**, **GUEST**, and **REFERRER** have read-only access. +Partners and debitors use the RBAC roles of the associated relations. + +## Considered Alternatives + +* **1. Replace Relations:** Replace PARTNER/DEBITOR/OPERATIONS/... relations with a new relation for the new partner person (e.g., heir community) as the new Holder via PATCH on /api/hs/office/partners/UUID +* **2. Directly Update Relations:** Change the Holder reference in the existing PARTNER relation to the new partner person (e.g., heir community) via PATCH on /api/hs/office/relations/UUID +* **3. Update Relations via Partner:** Change the partner person in the PARTNER relation via PATCH on /api/hs/office/partners/UUID + +### Option 1: Replace Relations + +The exchange of the partner (and debitor) person is done by creating a new PARTNER or DEBITOR relation, and then updating the reference in the partner or debitor to point to the new relation instead of the old one. + +#### Advantages + +- **Preserving the API:** This behavior is already implemented and requires no major API remodelling, only an extension to swap additional relations. +- **UPDATE permission for AGENT:** The AGENT role of a relation could be granted UPDATE rights because only the non-critical contact reference would be modifiable. +- **Congruence of business logic and API:** Conceptually, this aligns with replacing the partner person, though technically, a new PARTNER relation is created instead of directly replacing the person. + +#### Disadvantages + +- **Loss of explicit GRANTs:** Explicit GRANTs on the PARTNER relation would be lost due to the relation being replaced. Preserving these would require additional implementation effort. +- **Mismatch between business logic and API:** The exchange of the partner person would not directly occur at the partner but rather through a new PARTNER relation. +- **Not applicable to dependent relations:** Updating dependent relations (e.g., representatives, operational contacts, billing contacts, mailing list subscriptions) would require creating new relations and deleting old ones, again leading to the loss of explicit GRANTs. +- **Performance issues with many dependent relations:** Dependent relations can only be exchanged via loops rather than direct SQL UPDATEs, leading to poorer performance. + +### Option 2: Directly Update Relations + +The existing PARTNER relation remains unchanged, and the Holder is switched from the deceased person to the heir community. + +#### Advantages + +- **Applicability to both partner and debitor persons:** This approach would work for both partner and debitor persons at a generic level. +- **API uniformity and generality:** REST API changes would belong uniformly to the relation endpoint, consistent with how contact changes are currently handled. + +#### Disadvantages + +- **UPDATE permission for relation-AGENT would be problematic:** The relation-AGENT must not have permission to swap the Holder. Since there are no column-specific update rights, they would also lose the ability to change the Contact. +- **API remodelling:** The exchange of a partner person would move from the partner endpoint (/api/hs/office/partner) to the relation endpoint, requiring significant restructuring, including tests. +- **Mismatch between business logic and API:** Although conceptually it involves replacing a partner person, technically, the change would occur at the PARTNER relation. + +### Option 3: Update Relations via Partner + +The partner (or debitor) person would still be updated at the partner or debitor level, but instead of creating a new relation, the reference to the person would be updated in the existing PARTNER (or DEBITOR) relation. Dependent relations could be updated efficiently via SQL UPDATE. + +#### Advantages + +- **Preserving the API:** The endpoint /api/hs/office/partners/UUID remains unchanged, requiring only internal adjustments. +- **UPDATE permission for AGENT:** AGENT roles could be granted UPDATE rights, while API controls could limit modifications. +- **Congruence of business logic and API:** The technical implementation matches the conceptual model of replacing a partner person. + +#### Disadvantages + +No significant drawbacks were identified, other than allowing UPDATE permissions on relations while controlling updates at the API level. + +## Decision and Outcome + +**Decision:** 3. Update Relations via Partner + +**Rationale:** +- The API accurately reflects the business logic (PATCH partner person on /api/hs/office/partners/UUID) +- The effort required is relatively low (many updates can be done via SQL UPDATEs) +- UPDATE permission can be granted to relation-AGENT without security risks (since the API controls access) + + | Criteria \ Relations ... | 1. Replace | 2. Directly Update | 3. Update via Partner | + |------------------------------------------------|-----------:|-------------------:|----------------------:| + | **Technical and Effort Criteria** | | | | + | Preserve API vs. Remodelling (incl. risk) | +2 | -2 | +1 | + | Applicability to Partner and Debitor Person | | +1 | | + | Applicability to dependent relations | -3 | | | + | Performance with many dependent relations | -1 | | | + | Effort for explicit grants | -1 | | | + | **Intermediate Score** | **-3** | **-1** | **+1** | + | | | | | + | **Business Criteria** | | | | + | Congruence of Business Logic and API | +1 | -1 | +1 | + | Uniformity/Generality of the API | | +1 | | + | UPDATE Permission for Relation-AGENT possible | +1 | | +1 | + | **Intermediate Score** | **+2** | **0** | **+2** | + | | | | | + | **Final Score** | **-1** | **-1** | **+3** | + + -- 2.39.5 From 573ac44b625bee2c7af4343dae645e111c4472ca Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 4 Mar 2025 11:15:53 +0100 Subject: [PATCH 07/13] code-cleanup --- .../partner/HsOfficePartnerController.java | 42 +++++++++++-------- ...ficeRelationRepositoryIntegrationTest.java | 1 - 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index d2403078..8f70e1d1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -42,10 +42,10 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { private StrictMapper mapper; @Autowired - private HsOfficePartnerRbacRepository partnerRepo; + private HsOfficePartnerRbacRepository rbacPartnerRepo; @Autowired - private HsOfficeRelationRealRepository relationRepo; + private HsOfficeRelationRealRepository realRelationRepo; @PersistenceContext private EntityManager em; @@ -59,7 +59,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 +77,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 +98,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 +116,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 +133,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,12 +156,12 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { context.define(currentSubject, assumedRoles); - final var current = partnerRepo.findByUuid(partnerUuid).orElseThrow(); + final var current = rbacPartnerRepo.findByUuid(partnerUuid).orElseThrow(); final var previousPartnerRel = current.getPartnerRel(); new HsOfficePartnerEntityPatcher(em, current).apply(body); - final var saved = partnerRepo.save(current); + final var saved = rbacPartnerRepo.save(current); optionallyCreateExPartnerRelation(saved, previousPartnerRel); optionallyUpdateRelatedRelations(saved, previousPartnerRel); @@ -170,19 +170,27 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { } private void optionallyCreateExPartnerRelation(final HsOfficePartnerRbacEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) { - if (!saved.getPartnerRel().getUuid().equals(previousPartnerRel.getUuid())) { - relationRepo.save(previousPartnerRel.toBuilder().uuid(null) + + final var partnerRelHasChanged = !saved.getPartnerRel().getUuid().equals(previousPartnerRel.getUuid()); + if (partnerRelHasChanged) { + realRelationRepo.save(previousPartnerRel.toBuilder().uuid(null) .type(EX_PARTNER).anchor(saved.getPartnerRel().getHolder()) .build()); } } private void optionallyUpdateRelatedRelations(final HsOfficePartnerRbacEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) { -// if (!saved.getPartnerRel().getUuid().equals(previousPartnerRel.getUuid())) { -// relationRepo.save(previousPartnerRel.toBuilder().uuid(null) -// .type(EX_PARTNER).anchor(saved.getPartnerRel().getHolder()) -// .build()); -// } + final var oldPartnerPerson = previousPartnerRel.getHolder(); + final var newPartnerPerson = saved.getPartnerRel().getHolder(); + final var partnerPersonHasChanged = !newPartnerPerson.getUuid().equals(oldPartnerPerson.getUuid()); + if (partnerPersonHasChanged) { +// realRelationRepo.findRelationRelatedToPersonUuid(oldPartnerPerson.getUuid()).forEach( rel -> { +// if (rel.getType() == REPRESENTATIVE && rel.getAnchor().getUuid().equals(oldPartnerPerson.getUuid()) ) { +// +// } +// +// } + } } private HsOfficePartnerRbacEntity createPartnerEntity(final HsOfficePartnerInsertResource body) { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java index 4a9a1cee..5e8c750b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java @@ -28,7 +28,6 @@ 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; -- 2.39.5 From 839703ca48a705649452b61f3c881a0fdf3d62bf Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 4 Mar 2025 12:20:40 +0100 Subject: [PATCH 08/13] relation with updatable holder+anchor and test --- ...-02-27-exchanging-the-partner-person.de.md | 30 ++++++++-------- .../partner/HsOfficePartnerController.java | 7 +--- .../relation/HsOfficeRelationRbacEntity.java | 2 +- .../5033-hs-office-relation-rbac.sql | 14 -------- ...ficeRelationRepositoryIntegrationTest.java | 34 +++++++++++++++++++ 5 files changed, 51 insertions(+), 36 deletions(-) diff --git a/doc/adr/2025-02-27-exchanging-the-partner-person.de.md b/doc/adr/2025-02-27-exchanging-the-partner-person.de.md index e56aa28d..3367cbf2 100644 --- a/doc/adr/2025-02-27-exchanging-the-partner-person.de.md +++ b/doc/adr/2025-02-27-exchanging-the-partner-person.de.md @@ -105,20 +105,20 @@ Nennenswerte Nachteile wurden nicht identifiziert, allenfalls ist es etwas schr - der Aufwand ist relativ gering (vieles ist mit SQL UPDATEs machbar) - die UPDATE Permission dürfte an Relation-AGENT granted werden, ohne damit Schindluder getrieben werden kann (weil das an der API verhindert werden kann) -| Kriterium \ Relations ... | 1. ersetzen | 2. direkt aktualisieren | 3. via Partner aktualisieren | -|----------------------------------------------|------------:|------------------------:|-----------------------------:| -| **Technische und Aufwands-Kriterien** | | | | +| Kriterium \ Relations ... | 1. ersetzen | 2. direkt aktualisieren | 3. via Partner aktualisieren | +|-----------------------------------------------|------------:|------------------------:|-----------------------------:| +| **Technische und Aufwands-Kriterien** | | | | | Beibehaltung der API vs. Umbau (inkl. Risiko) | +2 | -2 | +1 | | Anwendbarkeit auf Partner- und Debitor-Person | | +1 | | -| Anwendbarkeit auf abhängige Relations | -3 | | | -| Performance bei vielen abhängigen Relations | -1 | | | -| Aufwand für explizite Grants | -1 | | | -| **Zwischenergebnis** | **-3** | **-1** | **+1** | -| | | | | -| **Fachliche Kriterien** | | | | -| Kongruenz von Fachlichkeit+API | +1 | -1 | +1 | -| Einheitlichkeit/Generizität der API | | +1 | | -| UPDATE Permission für Relation-AGENT möglich | +1 | | +1 | -| **Zwischenergebnis** | **+2** | **0** | **+2* | -| | | | | -| **Endergebnis** | **-1** | **-1** | **+3** | +| Anwendbarkeit auf abhängige Relations | -3 | | | +| Performance bei vielen abhängigen Relations | -1 | | | +| Aufwand für explizite Grants | -1 | | | +| **Zwischenergebnis** | **-3** | **-1** | **+1** | +| | | | | +| **Fachliche Kriterien** | | | | +| Kongruenz von Fachlichkeit+API | +1 | -1 | +1 | +| Einheitlichkeit/Generizität der API | | +1 | | +| UPDATE Permission für Relation-AGENT möglich | +1 | | +1 | +| **Zwischenergebnis** | **+2** | **0** | **+2** | +| | | | | +| **Endergebnis** | **-1** | **-1** | **+3** | diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index 8f70e1d1..c535c919 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -184,12 +184,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { final var newPartnerPerson = saved.getPartnerRel().getHolder(); final var partnerPersonHasChanged = !newPartnerPerson.getUuid().equals(oldPartnerPerson.getUuid()); if (partnerPersonHasChanged) { -// realRelationRepo.findRelationRelatedToPersonUuid(oldPartnerPerson.getUuid()).forEach( rel -> { -// if (rel.getType() == REPRESENTATIVE && rel.getAnchor().getUuid().equals(oldPartnerPerson.getUuid()) ) { -// -// } -// -// } + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacEntity.java index 35063799..d1f43923 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacEntity.java @@ -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("anchorUuid", "holderUuid", "contactUuid") + .withUpdatableColumns("anchorUuid", "holderUuid", "contactUuid") // BEWARE: additional checks at API-level .importEntityAlias("anchorPerson", HsOfficePersonRbacEntity.class, usingDefaultCase(), dependsOnColumn("anchorUuid"), directlyFetchedByDependsOnColumn(), diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql index 97e5bed8..8e1507d4 100644 --- a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql @@ -309,17 +309,3 @@ END; $$; --// - --- ============================================================================ ---changeset RbacRbacSystemRebuildGenerator:hs-office-relation-rbac-actually-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--// --- ---------------------------------------------------------------------------- - -begin transaction; - call base.defineContext( - 're-creating RBAC for table hs_office.relation', - null, - 'superuser-alex@hostsharing.net' -- FIXME: use env-var - ); - call hs_office.relation_rebuild_rbac_system(); -commit; ---// diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java index 5e8c750b..d316274d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java @@ -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 -- 2.39.5 From f645b9753382f9e67cceac145a579295479bf89f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 4 Mar 2025 17:16:27 +0100 Subject: [PATCH 09/13] amend API to patch partner person directly into partner --- .../partner/HsOfficePartnerController.java | 25 ++--- .../partner/HsOfficePartnerEntityPatcher.java | 17 ++- .../relation/HsOfficeRelationController.java | 4 +- .../HsOfficeRelationEntityContactPatcher.java | 34 ++++++ .../HsOfficeRelationEntityPatcher.java | 13 ++- .../hs-office/hs-office-partner-schemas.yaml | 6 +- .../hs-office/hs-office-relation-schemas.yaml | 16 +++ .../hs-office-relations-with-uuid.yaml | 2 +- ...OfficePartnerControllerAcceptanceTest.java | 26 +++-- .../HsOfficePartnerEntityPatcherUnitTest.java | 101 ++++++++++++------ 10 files changed, 166 insertions(+), 78 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityContactPatcher.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index c535c919..7e7bf0b5 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -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) { } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java index ecf8adc9..16d313ac 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java @@ -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 { - 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()); } } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java index 4a92bd7f..53e2712b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java @@ -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); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityContactPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityContactPatcher.java new file mode 100644 index 00000000..c01ab209 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityContactPatcher.java @@ -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 { + + 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"); + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcher.java index d9e6244a..634edee9 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcher.java @@ -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 { +public class HsOfficeRelationEntityPatcher implements EntityPatcher { 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)); diff --git a/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml index 727c5b61..dd0def9a 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml @@ -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' diff --git a/src/main/resources/api-definition/hs-office/hs-office-relation-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-relation-schemas.yaml index 9b8d2e46..d74b5da0 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-relation-schemas.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-relation-schemas.yaml @@ -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 diff --git a/src/main/resources/api-definition/hs-office/hs-office-relations-with-uuid.yaml b/src/main/resources/api-definition/hs-office/hs-office-relations-with-uuid.yaml index b492f322..b49902cf 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-relations-with-uuid.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-relations-with-uuid.yaml @@ -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 diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java index 4eac16e8..36490dff 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java @@ -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; }); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcherUnitTest.java index b38c41ae..48758e12 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcherUnitTest.java @@ -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 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(); - } } -- 2.39.5 From 3030b91e2ca30ff7c6929c652becf92e5d74daff Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 6 Mar 2025 10:06:45 +0100 Subject: [PATCH 10/13] working API with patching partner person directly into partner --- .../partner/HsOfficePartnerController.java | 2 +- .../partner/HsOfficePartnerEntityPatcher.java | 8 +- .../HsOfficeRelationEntityPatcher.java | 45 ++-- .../hs-office/hs-office-partner-schemas.yaml | 1 + .../hs-office/hs-office-relation-schemas.yaml | 5 + .../HsOfficePartnerControllerRestTest.java | 8 - .../HsOfficePartnerEntityPatcherUnitTest.java | 15 +- ...HsOfficeRelationEntityPatcherUnitTest.java | 227 ++++++++++++++++++ .../HsOfficeRelationPatcherUnitTest.java | 11 +- ...ceDeceasedPartnerWithCommunityOfHeirs.java | 155 ++++++------ .../rbac/test/PatchUnitTestBase.java | 8 +- 11 files changed, 356 insertions(+), 129 deletions(-) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index 7e7bf0b5..a0bcc25d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -159,7 +159,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { 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 = rbacPartnerRepo.save(current); optionallyCreateExPartnerRelation(saved, previousPartnerPerson); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java index 16d313ac..5d36b602 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java @@ -3,15 +3,21 @@ 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.HsOfficeRelationEntityPatcher; import net.hostsharing.hsadminng.mapper.EntityPatcher; +import net.hostsharing.hsadminng.mapper.StrictMapper; import jakarta.persistence.EntityManager; class HsOfficePartnerEntityPatcher implements EntityPatcher { + + 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; } @@ -20,7 +26,7 @@ class HsOfficePartnerEntityPatcher implements EntityPatcher { + private final StrictMapper mapper; private final EntityManager em; private final HsOfficeRelation entity; - public HsOfficeRelationEntityPatcher(final EntityManager em, final HsOfficeRelation entity) { + public HsOfficeRelationEntityPatcher(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) { - 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)); - }); - } + 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())); + } + } - private void verifyNotNull(final UUID newValue, final String propertyName) { - if (newValue == null) { - throw new IllegalArgumentException("property '" + propertyName + "' must not be null"); + 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())); + } } } } diff --git a/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml index dd0def9a..88b6fd06 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml @@ -52,6 +52,7 @@ components: $ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationPatch' details: $ref: '#/components/schemas/HsOfficePartnerDetailsPatch' + additionalProperties: false HsOfficePartnerDetailsPatch: type: object diff --git a/src/main/resources/api-definition/hs-office/hs-office-relation-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-relation-schemas.yaml index d74b5da0..96865955 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-relation-schemas.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-relation-schemas.yaml @@ -53,10 +53,15 @@ components: 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: diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java index 1ed5389b..9a688367 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java @@ -108,8 +108,6 @@ class HsOfficePartnerControllerRestTest { "holder.uuid": "%s", "contact.uuid": "%s" }, - "person.uuid": "%s", - "contact.uuid": "%s", "details": { "registrationOffice": "Temp Registergericht Aurich", "registrationNumber": "111111" @@ -118,8 +116,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 +141,6 @@ class HsOfficePartnerControllerRestTest { "holder.uuid": "%s", "contact.uuid": "%s" }, - "person.uuid": "%s", - "contact.uuid": "%s", "details": { "registrationOffice": "Temp Registergericht Aurich", "registrationNumber": "111111" @@ -155,8 +149,6 @@ class HsOfficePartnerControllerRestTest { """.formatted( GIVEN_MANDANTE_UUID, GIVEN_PERSON_UUID, - GIVEN_INVALID_UUID, - GIVEN_PERSON_UUID, GIVEN_INVALID_UUID)) .accept(MediaType.APPLICATION_JSON)) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcherUnitTest.java index 48758e12..e7f09b03 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcherUnitTest.java @@ -6,6 +6,8 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartne 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.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; @@ -14,7 +16,6 @@ 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; @@ -26,7 +27,7 @@ import static org.mockito.Mockito.lenient; @TestInstance(PER_CLASS) @ExtendWith(MockitoExtension.class) -// This test class does not rerive from PatchUnitTestBase because it has no directly patchable properties. +// 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 { @@ -47,13 +48,15 @@ class HsOfficePartnerEntityPatcherUnitTest { .build(); @Mock - private EntityManager em; + private EntityManagerWrapper emw; + + private StrictMapper mapper = new StrictMapper(emw); @BeforeEach void initMocks() { - lenient().when(em.getReference(eq(HsOfficePersonRealEntity.class), any())).thenAnswer(invocation -> + lenient().when(emw.getReference(eq(HsOfficePersonRealEntity.class), any())).thenAnswer(invocation -> HsOfficePersonRealEntity.builder().uuid(invocation.getArgument(1)).build()); - lenient().when(em.getReference(eq(HsOfficeContactRealEntity.class), any())).thenAnswer(invocation -> + lenient().when(emw.getReference(eq(HsOfficeContactRealEntity.class), any())).thenAnswer(invocation -> HsOfficeContactRealEntity.builder().uuid(invocation.getArgument(1)).build()); } @@ -123,6 +126,6 @@ class HsOfficePartnerEntityPatcherUnitTest { } protected HsOfficePartnerEntityPatcher createPatcher(final HsOfficePartnerRbacEntity partner) { - return new HsOfficePartnerEntityPatcher(em, partner); + return new HsOfficePartnerEntityPatcher(mapper, emw, partner); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java new file mode 100644 index 00000000..f4acb10b --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java @@ -0,0 +1,227 @@ +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.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.validation.ValidationException; +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.Mockito.lenient; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@TestInstance(PER_CLASS) +@ExtendWith(MockitoExtension.class) +class HsOfficeRelationEntityPatcherUnitTest extends PatchUnitTestBase< + HsOfficeRelationPatchResource, + HsOfficeRelation + > { + + 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() { + + { + setPersonType(HsOfficePersonTypeResource.NATURAL_PERSON); + setFamilyName("Patched-Holder-Family-Name"); + 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() { + + { + setCaption("Patched-Contact-Caption"); + } + }; + private static HsOfficeContactRealEntity PATCHED_CONTACT = HsOfficeContactRealEntity.builder() + .uuid(PATCHED_CONTACT_UUID) + .caption("Patched-Contact-Caption") + .build(); + + @Mock + private EntityManagerWrapper emw; + + private StrictMapper mapper = new StrictMapper(emw); + + @BeforeEach + void initMocks() { + 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 HsOfficeRelationRealEntity(); + entity.setUuid(INITIAL_RELATION_UUID); + 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; + } + + @Override + protected HsOfficeRelationPatchResource newPatchResource() { + return new HsOfficeRelationPatchResource(); + } + + @Override + protected HsOfficeRelationEntityPatcher createPatcher(final HsOfficeRelation relation) { + return new HsOfficeRelationEntityPatcher(mapper, emw, relation); + } + + @Override + protected Stream propertyTestDescriptors() { + return Stream.of( + new JsonNullableProperty<>( + "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, + PATCHED_CONTACT), + new SimpleProperty<>( + "contact", + HsOfficeRelationPatchResource::setContact, + CONTACT_PATCH_RESOURCE, + HsOfficeRelation::setContact, + withoutUuid(PATCHED_CONTACT)) + .notNullable() + ); + } + + @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(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationPatcherUnitTest.java index 343fcd90..72871086 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationPatcherUnitTest.java @@ -3,6 +3,8 @@ 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.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.TestInstance; @@ -10,7 +12,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import jakarta.persistence.EntityManager; import java.util.UUID; import java.util.stream.Stream; @@ -30,11 +31,13 @@ class HsOfficeRelationPatcherUnitTest extends PatchUnitTestBase< static final UUID PATCHED_CONTACT_UUID = UUID.randomUUID(); @Mock - EntityManager em; + EntityManagerWrapper emw; + + StrictMapper mapper = new StrictMapper(emw); @BeforeEach void initMocks() { - lenient().when(em.getReference(eq(HsOfficeContactRealEntity.class), any())).thenAnswer(invocation -> + lenient().when(emw.getReference(eq(HsOfficeContactRealEntity.class), any())).thenAnswer(invocation -> HsOfficeContactRealEntity.builder().uuid(invocation.getArgument(1)).build()); } @@ -66,7 +69,7 @@ class HsOfficeRelationPatcherUnitTest extends PatchUnitTestBase< @Override protected HsOfficeRelationEntityPatcher createPatcher(final HsOfficeRelation relation) { - return new HsOfficeRelationEntityPatcher(em, relation); + return new HsOfficeRelationEntityPatcher(mapper, emw, relation); } @Override diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java index c706e8e6..c114809b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java @@ -12,101 +12,81 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase - 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,7 +100,8 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase 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}") ); // TODO.test: Verify the EX_PARTNER-Relation, once we fixed the anchor problem, see HsOfficePartnerController @@ -128,9 +109,11 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase httpGet("/api/hs/office/relations?relationType=REPRESENTATIVE&personUuid=%{Person: %{representativeGivenName} %{representativeFamilyName}}") + () -> httpGet( + "/api/hs/office/relations?relationType=REPRESENTATIVE&personUuid=%{Person: %{representativeGivenName} %{representativeFamilyName}}") .expecting(OK).expecting(JSON).expectArrayElements(1), - path("[0].anchor.tradeName").contains("Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"), + path("[0].anchor.tradeName").contains( + "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"), path("[0].holder.familyName").contains("%{representativeFamilyName}") ); diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/PatchUnitTestBase.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/PatchUnitTestBase.java index f2b7e8bb..82d133e4 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/PatchUnitTestBase.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/PatchUnitTestBase.java @@ -19,7 +19,7 @@ import static org.assertj.core.api.Assumptions.assumeThat; public abstract class PatchUnitTestBase { @Test - void willPatchNoProperty() { + protected void willPatchNoProperty() { // given final var givenEntity = newInitialEntity(); final var patchResource = newPatchResource(); @@ -73,7 +73,7 @@ public abstract class PatchUnitTestBase { @ParameterizedTest @MethodSource("propertyTestCases") - void willThrowExceptionIfNotNullableValueIsNull(final Property testCase) { + protected void willThrowExceptionIfNotNullableValueIsNull(final Property testCase) { assumeThat(testCase instanceof JsonNullableProperty).isTrue(); assumeThat(testCase.nullable).isFalse(); @@ -94,7 +94,7 @@ public abstract class PatchUnitTestBase { @ParameterizedTest @MethodSource("propertyTestCases") - void willPatchOnlyGivenPropertyToNull(final Property testCase) { + protected void willPatchOnlyGivenPropertyToNull(final Property testCase) { assumeThat(testCase.nullable).isTrue(); // given @@ -113,7 +113,7 @@ public abstract class PatchUnitTestBase { @ParameterizedTest @MethodSource("propertyTestCases") - void willNotPatchIfGivenPropertyNotGiven(final Property testCase) { + protected void willNotPatchIfGivenPropertyNotGiven(final Property testCase) { // given final var givenEntity = newInitialEntity(); -- 2.39.5 From 6d3f191d1ae0c32afd4cd0629fe0894f7640965d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 6 Mar 2025 11:35:13 +0100 Subject: [PATCH 11/13] amend API to patch partner person directly into partner --- .../partner/HsOfficePartnerController.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index a0bcc25d..f7440eaf 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -185,7 +185,22 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { private void optionallyUpdateRelatedRelations(final HsOfficePartnerRbacEntity saved, final HsOfficePersonRealEntity previousPartnerPerson) { final var partnerPersonHasChanged = !saved.getPartnerRel().getHolder().getUuid().equals(previousPartnerPerson.getUuid()); if (partnerPersonHasChanged) { - + em.createNativeQuery(""" + UPDATE hs_office.relation + SET anchorUuid = :newPartnerPersonUuid + WHERE anchorUuid = :oldPartnerPersonUuid + """) + .setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid()) + .setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid()) + .executeUpdate(); + em.createNativeQuery(""" + UPDATE hs_office.relation + SET holderUuid = :newPartnerPersonUuid + WHERE holderUuid = :oldPartnerPersonUuid AND type = 'DEBITOR' + """) + .setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid()) + .setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid()) + .executeUpdate(); } } -- 2.39.5 From 9f1bb284e7597bf22f24c51ced8101662cd299cb Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 6 Mar 2025 13:38:24 +0100 Subject: [PATCH 12/13] add scenario shouldCreateSelfDebitorForPartnerWithIdenticalContactData --- .../partner/HsOfficePartnerController.java | 11 ++--- .../scenarios/HsOfficeScenarioTests.java | 21 ++++++++- ...torForPartnerWithIdenticalContactData.java | 45 +++++++++++++++++++ 3 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartnerWithIdenticalContactData.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index f7440eaf..2c519849 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -185,18 +185,19 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { private void optionallyUpdateRelatedRelations(final HsOfficePartnerRbacEntity saved, final HsOfficePersonRealEntity previousPartnerPerson) { final var partnerPersonHasChanged = !saved.getPartnerRel().getHolder().getUuid().equals(previousPartnerPerson.getUuid()); if (partnerPersonHasChanged) { - em.createNativeQuery(""" + final var count = em.createNativeQuery(""" UPDATE hs_office.relation - SET anchorUuid = :newPartnerPersonUuid - WHERE anchorUuid = :oldPartnerPersonUuid + SET holderUuid = :newPartnerPersonUuid + WHERE type = 'DEBITOR' AND holderUuid = :oldPartnerPersonUuid AND anchorUuid = :oldPartnerPersonUuid """) .setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid()) .setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid()) .executeUpdate(); + System.out.println(count); // FIXME: remove em.createNativeQuery(""" UPDATE hs_office.relation - SET holderUuid = :newPartnerPersonUuid - WHERE holderUuid = :oldPartnerPersonUuid AND type = 'DEBITOR' + SET anchorUuid = :newPartnerPersonUuid + WHERE anchorUuid = :oldPartnerPersonUuid """) .setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid()) .setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid()) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java index 646896e9..e643ea8a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/HsOfficeScenarioTests.java @@ -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") diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartnerWithIdenticalContactData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartnerWithIdenticalContactData.java new file mode 100644 index 00000000..1cb08ce4 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/debitor/CreateSelfDebitorForPartnerWithIdenticalContactData.java @@ -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 { + + 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); + } + +} -- 2.39.5 From 01c86fa782150ff7557f1d63181692aff6372f96 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 6 Mar 2025 15:49:20 +0100 Subject: [PATCH 13/13] HsOfficeContactFromResourceConverter --- .../contact/HsOfficeContactController.java | 24 +- .../HsOfficeContactFromResourceConverter.java | 27 +++ .../partner/HsOfficePartnerController.java | 20 +- .../partner/HsOfficePartnerEntityPatcher.java | 4 +- ...cher.java => HsOfficeRelationPatcher.java} | 4 +- .../hsadminng/arch/ArchitectureTest.java | 3 +- .../HsOfficePartnerControllerRestTest.java | 3 +- ...HsOfficeRelationEntityPatcherUnitTest.java | 227 ------------------ .../HsOfficeRelationPatcherUnitTest.java | 210 +++++++++++++--- ...ceDeceasedPartnerWithCommunityOfHeirs.java | 33 ++- .../hsadminng/hs/scenarios/PathAssertion.java | 13 + .../hs/scenarios/TemplateResolver.java | 14 ++ 12 files changed, 299 insertions(+), 283 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactFromResourceConverter.java rename src/main/java/net/hostsharing/hsadminng/hs/office/relation/{HsOfficeRelationEntityPatcher.java => HsOfficeRelationPatcher.java} (90%) delete mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java index 0a041763..bf8a9c09 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java @@ -13,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 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 RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { - entity.putPostalAddress(from(resource.getPostalAddress())); - entity.putEmailAddresses(from(resource.getEmailAddresses())); - entity.putPhoneNumbers(from(resource.getPhoneNumbers())); - }; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactFromResourceConverter.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactFromResourceConverter.java new file mode 100644 index 00000000..8ebcbc88 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactFromResourceConverter.java @@ -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 + implements Converter { + + @Override + @SneakyThrows + public E convert(final MappingContext context) { + final var resource = context.getSource(); + final var entity = context.getDestinationType().getDeclaredConstructor().newInstance(); + entity.setCaption(resource.getCaption()); + entity.putPostalAddress(from(resource.getPostalAddress())); + entity.putEmailAddresses(from(resource.getEmailAddresses())); + entity.putPhoneNumbers(from(resource.getPhoneNumbers())); + return entity; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index 2c519849..6074c909 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -3,8 +3,10 @@ package net.hostsharing.hsadminng.hs.office.partner; import io.micrometer.core.annotation.Timed; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.errors.ReferenceNotFoundException; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePartnersApi; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource; @@ -22,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import jakarta.annotation.PostConstruct; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import java.util.List; @@ -41,6 +44,9 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { @Autowired private StrictMapper mapper; + @Autowired + private HsOfficeContactFromResourceConverter contactFromResourceConverter; + @Autowired private HsOfficePartnerRbacRepository rbacPartnerRepo; @@ -50,6 +56,11 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { @PersistenceContext private EntityManager em; + @PostConstruct + public void init() { + mapper.addConverter(contactFromResourceConverter, HsOfficeContactInsertResource.class, HsOfficeContactRealEntity.class); + } + @Override @Transactional(readOnly = true) @Timed("app.office.partners.api.getListOfPartners") @@ -185,15 +196,18 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { private void optionallyUpdateRelatedRelations(final HsOfficePartnerRbacEntity saved, final HsOfficePersonRealEntity previousPartnerPerson) { final var partnerPersonHasChanged = !saved.getPartnerRel().getHolder().getUuid().equals(previousPartnerPerson.getUuid()); if (partnerPersonHasChanged) { - final var count = em.createNativeQuery(""" + // 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 + WHERE type = 'DEBITOR' AND + holderUuid = :oldPartnerPersonUuid AND anchorUuid = :oldPartnerPersonUuid """) .setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid()) .setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid()) .executeUpdate(); - System.out.println(count); // FIXME: remove + + // re-anchor all relations from the old partner person to the new partner persion em.createNativeQuery(""" UPDATE hs_office.relation SET anchorUuid = :newPartnerPersonUuid diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java index 5d36b602..5c176f93 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java @@ -1,7 +1,7 @@ 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.HsOfficeRelationEntityPatcher; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationPatcher; import net.hostsharing.hsadminng.mapper.EntityPatcher; import net.hostsharing.hsadminng.mapper.StrictMapper; @@ -26,7 +26,7 @@ class HsOfficePartnerEntityPatcher implements EntityPatcher { +public class HsOfficeRelationPatcher implements EntityPatcher { private final StrictMapper mapper; private final EntityManager em; private final HsOfficeRelation entity; - public HsOfficeRelationEntityPatcher(final StrictMapper mapper, final EntityManager em, final HsOfficeRelation entity) { + public HsOfficeRelationPatcher(final StrictMapper mapper, final EntityManager em, final HsOfficeRelation entity) { this.mapper = mapper; this.em = em; this.entity = entity; diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index 2bf87f09..664b803c 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -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( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java index 9a688367..074b3580 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java @@ -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 { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java deleted file mode 100644 index f4acb10b..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java +++ /dev/null @@ -1,227 +0,0 @@ -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.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.validation.ValidationException; -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.Mockito.lenient; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -@TestInstance(PER_CLASS) -@ExtendWith(MockitoExtension.class) -class HsOfficeRelationEntityPatcherUnitTest extends PatchUnitTestBase< - HsOfficeRelationPatchResource, - HsOfficeRelation - > { - - 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() { - - { - setPersonType(HsOfficePersonTypeResource.NATURAL_PERSON); - setFamilyName("Patched-Holder-Family-Name"); - 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() { - - { - setCaption("Patched-Contact-Caption"); - } - }; - private static HsOfficeContactRealEntity PATCHED_CONTACT = HsOfficeContactRealEntity.builder() - .uuid(PATCHED_CONTACT_UUID) - .caption("Patched-Contact-Caption") - .build(); - - @Mock - private EntityManagerWrapper emw; - - private StrictMapper mapper = new StrictMapper(emw); - - @BeforeEach - void initMocks() { - 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 HsOfficeRelationRealEntity(); - entity.setUuid(INITIAL_RELATION_UUID); - 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; - } - - @Override - protected HsOfficeRelationPatchResource newPatchResource() { - return new HsOfficeRelationPatchResource(); - } - - @Override - protected HsOfficeRelationEntityPatcher createPatcher(final HsOfficeRelation relation) { - return new HsOfficeRelationEntityPatcher(mapper, emw, relation); - } - - @Override - protected Stream propertyTestDescriptors() { - return Stream.of( - new JsonNullableProperty<>( - "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, - PATCHED_CONTACT), - new SimpleProperty<>( - "contact", - HsOfficeRelationPatchResource::setContact, - CONTACT_PATCH_RESOURCE, - HsOfficeRelation::setContact, - withoutUuid(PATCHED_CONTACT)) - .notNullable() - ); - } - - @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(); - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationPatcherUnitTest.java index 72871086..8647fc19 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationPatcherUnitTest.java @@ -1,24 +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.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) @@ -27,38 +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 - EntityManagerWrapper emw; + private EntityManagerWrapper emw; + + private StrictMapper mapper; - StrictMapper mapper = new StrictMapper(emw); @BeforeEach - void initMocks() { - lenient().when(emw.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; } @@ -68,24 +128,114 @@ class HsOfficeRelationPatcherUnitTest extends PatchUnitTestBase< } @Override - protected HsOfficeRelationEntityPatcher createPatcher(final HsOfficeRelation relation) { - return new HsOfficeRelationEntityPatcher(mapper, emw, relation); + protected HsOfficeRelationPatcher createPatcher(final HsOfficeRelation relation) { + return new HsOfficeRelationPatcher(mapper, emw, relation); } @Override protected Stream 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(); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java index c114809b..1db638df 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/partner/ReplaceDeceasedPartnerWithCommunityOfHeirs.java @@ -101,22 +101,43 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase httpGet("/api/hs/office/partners/%{partnerNumber}") .expecting(OK).expecting(JSON).expectObject(), path("partnerRel.holder.tradeName").contains( - "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}") + "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"), + path("partnerRel.contact.postalAddress").lenientlyContainsJson("§{communityOfHeirsPostalAddress}"), + path("partnerRel.contact.phoneNumbers.office").contains("%{communityOfHeirsOfficePhoneNumber}"), + path("partnerRel.contact.emailAddresses.main").contains("%{communityOfHeirsEmailAddress}") ); - // 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}}") + "/api/hs/office/relations?relationType=REPRESENTATIVE&personUuid=%{Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}") .expecting(OK).expecting(JSON).expectArrayElements(1), path("[0].anchor.tradeName").contains( "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"), - path("[0].holder.familyName").contains("%{representativeFamilyName}") + path("[0].holder.familyName").contains("%{representativeFamilyName}"), + path("[0].contact.postalAddress").lenientlyContainsJson("§{communityOfHeirsPostalAddress}"), + path("[0].contact.phoneNumbers.office").contains("%{communityOfHeirsOfficePhoneNumber}"), + path("[0].contact.emailAddresses.main").contains("%{communityOfHeirsEmailAddress}") ); - // 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}") + ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java index 4a51932e..04ec0fe9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/PathAssertion.java @@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.scenarios.UseCase.HttpResponse; import java.util.function.Consumer; import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS; +import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals; import static org.junit.jupiter.api.Assertions.fail; public class PathAssertion { @@ -27,6 +28,18 @@ public class PathAssertion { }; } + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Consumer lenientlyContainsJson(final String resolvableValue) { + return response -> { + try { + lenientlyEquals(ScenarioTest.resolve(resolvableValue, DROP_COMMENTS)).matches(response.getFromBody(path)) ; + } catch (final AssertionError e) { + // without this, the error message is often lacking important context + fail(e.getMessage() + " in `path(\"" + path + "\").contains(\"" + resolvableValue + "\")`" ); + } + }; + } + public Consumer doesNotExist() { return response -> { try { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java index d28adb26..eb82d676 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/TemplateResolver.java @@ -32,6 +32,12 @@ public class TemplateResolver { return jsonQuoted(value); } }, + JSON_OBJECT('§'){ + @Override + String convert(final Object value, final Resolver resolver) { + return jsonObject(value); + } + }, URI_ENCODED('&'){ @Override String convert(final Object value, final Resolver resolver) { @@ -213,4 +219,12 @@ public class TemplateResolver { default -> "\"" + value + "\""; }; } + + private static String jsonObject(final Object value) { + return switch (value) { + case null -> null; + case String string -> "{" + string.replace("\n", " ") + "}"; + default -> throw new IllegalArgumentException("can not format " + value.getClass() + " (" + value + ") as JSON object"); + }; + } } -- 2.39.5