From 64461fc4dae56b3e52a5120c5fca1952f6fd5ca2 Mon Sep 17 00:00:00 2001
From: Michael Hoennig <michael@hoennig.de>
Date: Fri, 28 Oct 2022 13:44:48 +0200
Subject: [PATCH] SEPA-Mandate signed date and patcher

---
 src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityPatcher.java             |   26 +++++
 src/main/java/net/hostsharing/hsadminng/mapper/PostgresDateRange.java                                           |   27 +++-
 src/main/resources/api-definition/hs-office/api-mappings.yaml                                                   |    2 
 src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java |   19 ++-
 src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java                |   18 ++-
 src/main/resources/db/changelog/250-hs-office-sepamandate.sql                                                   |    1 
 src/main/resources/api-definition/hs-office/hs-office-sepamandate-schemas.yaml                                  |   20 ++++
 src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql                                              |    1 
 src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityUnitTest.java            |   21 ++++
 src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java  |    6 +
 src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java                    |   24 ++++
 src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java              |   13 ++
 src/main/resources/db/changelog/258-hs-office-sepamandate-test-data.sql                                         |    4 
 src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java                      |   15 ++
 src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityPatcherUnitTest.java     |   98 +++++++++++++++++++
 15 files changed, 261 insertions(+), 34 deletions(-)

diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java
index d887e35..7bcdc86 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java
@@ -18,7 +18,7 @@
 import java.time.LocalDate;
 import java.util.UUID;
 
-import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
+import static net.hostsharing.hsadminng.mapper.PostgresDateRange.*;
 import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
 
 @Entity
@@ -73,13 +73,22 @@
     private HsOfficeReasonForTermination reasonForTermination;
 
     public void setValidFrom(final LocalDate validFrom) {
-        validity = toPostgresDateRange(validFrom, getValidity().upper());
+        setValidity(toPostgresDateRange(validFrom, getValidTo()));
     }
 
     public void setValidTo(final LocalDate validTo) {
-        validity = toPostgresDateRange(getValidity().lower(), validTo);
+        setValidity(toPostgresDateRange(getValidFrom(), validTo));
     }
 
+    public LocalDate getValidFrom() {
+        return lowerInclusiveFromPostgresDateRange(getValidity());
+    }
+
+    public LocalDate getValidTo() {
+        return upperInclusiveFromPostgresDateRange(getValidity());
+    }
+
+
     public Range<LocalDate> getValidity() {
         if (validity == null) {
             validity = Range.infinite(LocalDate.class);
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java
index 60983c1..2b8ad07 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java
@@ -1,5 +1,7 @@
 package net.hostsharing.hsadminng.hs.office.sepamandate;
 
+import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntityPatcher;
+import net.hostsharing.hsadminng.mapper.Mapper;
 import net.hostsharing.hsadminng.context.Context;
 import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeSepaMandatesApi;
 import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateInsertResource;
@@ -32,7 +34,7 @@
     private Mapper mapper;
 
     @Autowired
-    private HsOfficeSepaMandateRepository SepaMandateRepo;
+    private HsOfficeSepaMandateRepository sepaMandateRepo;
 
     @PersistenceContext
     private EntityManager em;
@@ -45,7 +47,7 @@
             final String iban) {
         context.define(currentUser, assumedRoles);
 
-        final var entities = SepaMandateRepo.findSepaMandateByOptionalIban(iban);
+        final var entities = sepaMandateRepo.findSepaMandateByOptionalIban(iban);
 
         final var resources = mapper.mapList(entities, HsOfficeSepaMandateResource.class,
                 SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER);
@@ -63,7 +65,7 @@
 
         final var entityToSave = mapper.map(body, HsOfficeSepaMandateEntity.class, SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER);
 
-        final var saved = SepaMandateRepo.save(entityToSave);
+        final var saved = sepaMandateRepo.save(entityToSave);
 
         final var uri =
                 MvcUriComponentsBuilder.fromController(getClass())
@@ -84,7 +86,7 @@
 
         context.define(currentUser, assumedRoles);
 
-        final var result = SepaMandateRepo.findByUuid(sepaMandateUuid);
+        final var result = sepaMandateRepo.findByUuid(sepaMandateUuid);
         if (result.isEmpty()) {
             return ResponseEntity.notFound().build();
         }
@@ -100,7 +102,7 @@
             final UUID sepaMandateUuid) {
         context.define(currentUser, assumedRoles);
 
-        final var result = SepaMandateRepo.deleteByUuid(sepaMandateUuid);
+        final var result = sepaMandateRepo.deleteByUuid(sepaMandateUuid);
         if (result == 0) {
             return ResponseEntity.notFound().build();
         }
@@ -118,11 +120,11 @@
 
         context.define(currentUser, assumedRoles);
 
-        final var current = SepaMandateRepo.findByUuid(sepaMandateUuid).orElseThrow();
+        final var current = sepaMandateRepo.findByUuid(sepaMandateUuid).orElseThrow();
 
-        current.setValidity(toPostgresDateRange(current.getValidity().lower(), body.getValidTo()));
+        new HsOfficeSepaMandateEntityPatcher(current).apply(body);
 
-        final var saved = SepaMandateRepo.save(current);
+        final var saved = sepaMandateRepo.save(current);
         final var mapped = mapper.map(saved, HsOfficeSepaMandateResource.class, SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER);
         return ResponseEntity.ok(mapped);
     }
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java
index a4120d8..a07770a 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java
@@ -14,6 +14,7 @@
 import java.time.LocalDate;
 import java.util.UUID;
 
+import static net.hostsharing.hsadminng.mapper.PostgresDateRange.*;
 import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
 
 @Entity
@@ -33,6 +34,7 @@
     private static Stringify<HsOfficeSepaMandateEntity> stringify = stringify(HsOfficeSepaMandateEntity.class)
             .withProp(e -> e.getBankAccount().getIban())
             .withProp(HsOfficeSepaMandateEntity::getReference)
+            .withProp(HsOfficeSepaMandateEntity::getAgreement)
             .withProp(e -> e.getValidity().asString())
             .withSeparator(", ")
             .quotedValues(false);
@@ -51,8 +53,27 @@
 
     private @Column(name = "reference") String reference;
 
+    @Column(name="agreement", columnDefinition = "date")
+    private LocalDate agreement;
+
     @Column(name = "validity", columnDefinition = "daterange")
-    private Range<LocalDate> validity;
+    private Range<LocalDate> validity = Range.infinite(LocalDate.class);
+
+    public void setValidFrom(final LocalDate validFrom) {
+        setValidity(toPostgresDateRange(validFrom, getValidTo()));
+    }
+
+    public void setValidTo(final LocalDate validTo) {
+        setValidity(toPostgresDateRange(getValidFrom(), validTo));
+    }
+
+    public LocalDate getValidFrom() {
+        return lowerInclusiveFromPostgresDateRange(getValidity());
+    }
+
+    public LocalDate getValidTo() {
+        return upperInclusiveFromPostgresDateRange(getValidity());
+    }
 
     @Override
     public String toString() {
@@ -63,4 +84,5 @@
     public String toShortString() {
         return reference;
     }
+
 }
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityPatcher.java
new file mode 100644
index 0000000..4d71577
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityPatcher.java
@@ -0,0 +1,26 @@
+package net.hostsharing.hsadminng.hs.office.sepamandate;
+
+import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandatePatchResource;
+import net.hostsharing.hsadminng.mapper.EntityPatcher;
+import net.hostsharing.hsadminng.mapper.OptionalFromJson;
+
+public class HsOfficeSepaMandateEntityPatcher implements EntityPatcher<HsOfficeSepaMandatePatchResource> {
+
+    private final HsOfficeSepaMandateEntity entity;
+
+    public HsOfficeSepaMandateEntityPatcher(final HsOfficeSepaMandateEntity entity) {
+        this.entity = entity;
+    }
+
+    @Override
+    public void apply(final HsOfficeSepaMandatePatchResource resource) {
+        OptionalFromJson.of(resource.getReference()).ifPresent(
+                entity::setReference);
+        OptionalFromJson.of(resource.getAgreement()).ifPresent(
+                entity::setAgreement);
+        OptionalFromJson.of(resource.getValidFrom()).ifPresent(
+                entity::setValidFrom);
+        OptionalFromJson.of(resource.getValidTo()).ifPresent(
+                entity::setValidTo);
+    }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PostgresDateRange.java b/src/main/java/net/hostsharing/hsadminng/mapper/PostgresDateRange.java
index fd70ed0..c360db1 100644
--- a/src/main/java/net/hostsharing/hsadminng/mapper/PostgresDateRange.java
+++ b/src/main/java/net/hostsharing/hsadminng/mapper/PostgresDateRange.java
@@ -9,14 +9,25 @@
 public class PostgresDateRange {
 
     public static Range<LocalDate> toPostgresDateRange(
-            final LocalDate validFrom,
-            final LocalDate validTo) {
-        return validFrom != null
-                ? validTo != null
-                    ? Range.closedOpen(validFrom, validTo.plusDays(1))
-                    : Range.closedInfinite(validFrom)
-                : validTo != null
-                    ? Range.infiniteOpen(validTo.plusDays(1))
+            final LocalDate lowerInclusive,
+            final LocalDate upperInclusive) {
+        return lowerInclusive != null
+                ? upperInclusive != null
+                    ? Range.closedOpen(lowerInclusive, upperInclusive.plusDays(1))
+                    : Range.closedInfinite(lowerInclusive)
+                : upperInclusive != null
+                    ? Range.infiniteOpen(upperInclusive.plusDays(1))
                     : Range.infinite(LocalDate.class);
     }
+
+    public static LocalDate lowerInclusiveFromPostgresDateRange(
+            final Range<LocalDate> range) {
+        return range.lower();
+    }
+
+    public static LocalDate upperInclusiveFromPostgresDateRange(
+        final Range<LocalDate> range) {
+        return range.upper() != null ? range.upper().minusDays(1) : null;
+    }
+
 }
diff --git a/src/main/resources/api-definition/hs-office/api-mappings.yaml b/src/main/resources/api-definition/hs-office/api-mappings.yaml
index 8f6f076..11778eb 100644
--- a/src/main/resources/api-definition/hs-office/api-mappings.yaml
+++ b/src/main/resources/api-definition/hs-office/api-mappings.yaml
@@ -29,7 +29,7 @@
             null: org.openapitools.jackson.nullable.JsonNullable
         /api/hs/office/debitors/{debitorUUID}:
             null: org.openapitools.jackson.nullable.JsonNullable
-        /api/hs/office/sepamandates/{debitorUUID}:
+        /api/hs/office/sepamandates/{sepaMandateUUID}:
             null: org.openapitools.jackson.nullable.JsonNullable
         /api/hs/office/memberships/{membershipUUID}:
             null: org.openapitools.jackson.nullable.JsonNullable
diff --git a/src/main/resources/api-definition/hs-office/hs-office-sepamandate-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-sepamandate-schemas.yaml
index e4189a0..dd2af7f 100644
--- a/src/main/resources/api-definition/hs-office/hs-office-sepamandate-schemas.yaml
+++ b/src/main/resources/api-definition/hs-office/hs-office-sepamandate-schemas.yaml
@@ -15,6 +15,9 @@
                     $ref: './hs-office-bankaccount-schemas.yaml#/components/schemas/HsOfficeBankAccount'
                 reference:
                    type: string
+                agreement:
+                    type: string
+                    format: date
                 validFrom:
                    type: string
                    format: date
@@ -25,9 +28,21 @@
         HsOfficeSepaMandatePatch:
             type: object
             properties:
+                reference:
+                    type: string
+                    nullable: true
+                agreement:
+                    type: string
+                    format: date
+                    nullable: true
+                validFrom:
+                    type: string
+                    format: date
+                    nullable: true
                 validTo:
                     type: string
                     format: date
+                    nullable: true
             additionalProperties: false
 
         HsOfficeSepaMandateInsert:
@@ -44,6 +59,10 @@
                 reference:
                     type: string
                     nullable: false
+                agreement:
+                    type: string
+                    format: date
+                    nullable: false
                 validFrom:
                     type: string
                     format: date
@@ -56,5 +75,6 @@
                 - debitorUuid
                 - bankAccountUuid
                 - reference
+                - agreement
                 - validFrom
             additionalProperties: false
diff --git a/src/main/resources/db/changelog/250-hs-office-sepamandate.sql b/src/main/resources/db/changelog/250-hs-office-sepamandate.sql
index b87b872..fa60716 100644
--- a/src/main/resources/db/changelog/250-hs-office-sepamandate.sql
+++ b/src/main/resources/db/changelog/250-hs-office-sepamandate.sql
@@ -10,6 +10,7 @@
     debitorUuid         uuid not null references hs_office_debitor(uuid),
     bankAccountUuid     uuid not null references hs_office_bankaccount(uuid),
     reference           varchar(96) not null,
+    agreement           date not null,
     validity            daterange not null
 );
 --//
diff --git a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql
index 85569e6..f63bf64 100644
--- a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql
+++ b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql
@@ -102,6 +102,7 @@
 call generateRbacRestrictedView('hs_office_sepamandate',
     orderby => 'target.reference',
     columnUpdates => $updates$
+        agreement = new.agreement,
         validity = new.validity
     $updates$);
 --//
diff --git a/src/main/resources/db/changelog/258-hs-office-sepamandate-test-data.sql b/src/main/resources/db/changelog/258-hs-office-sepamandate-test-data.sql
index d96bfcb..eb96d1a 100644
--- a/src/main/resources/db/changelog/258-hs-office-sepamandate-test-data.sql
+++ b/src/main/resources/db/changelog/258-hs-office-sepamandate-test-data.sql
@@ -31,8 +31,8 @@
     raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor;
     raise notice '- using bankAccount (%): %', relatedBankAccount.uuid, relatedBankAccount;
     insert
-        into hs_office_sepamandate (uuid, debitoruuid, bankAccountuuid, reference, validity)
-        values (uuid_generate_v4(), relatedDebitor.uuid, relatedBankAccount.uuid, 'ref'||idName, daterange('20221001' , '20261231', '[]'));
+        into hs_office_sepamandate (uuid, debitoruuid, bankAccountuuid, reference, agreement, validity)
+        values (uuid_generate_v4(), relatedDebitor.uuid, relatedBankAccount.uuid, 'ref'||idName, '20220930', daterange('20221001' , '20261231', '[]'));
 end; $$;
 --//
 
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java
index 2c07d14..8bc26cf 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java
@@ -15,6 +15,7 @@
 class HsOfficeMembershipEntityUnitTest {
 
     public static final LocalDate GIVEN_VALID_FROM = LocalDate.parse("2020-01-01");
+
     final HsOfficeMembershipEntity givenMembership = HsOfficeMembershipEntity.builder()
             .memberNumber(10001)
             .partner(TEST_PARTNER)
@@ -54,9 +55,19 @@
     }
 
     @Test
+    void settingValidFromKeepsValidTo() {
+        givenMembership.setValidFrom(LocalDate.parse("2020-01-01"));
+        assertThat(givenMembership.getValidFrom()).isEqualTo(LocalDate.parse("2020-01-01"));
+        assertThat(givenMembership.getValidTo()).isNull();
+
+    }
+
+    @Test
     void settingValidToKeepsValidFrom() {
         givenMembership.setValidTo(LocalDate.parse("2024-12-31"));
-        assertThat(givenMembership.getValidity().lower()).isEqualTo(GIVEN_VALID_FROM);
+        assertThat(givenMembership.getValidFrom()).isEqualTo(GIVEN_VALID_FROM);
+        assertThat(givenMembership.getValidTo()).isEqualTo(LocalDate.parse("2024-12-31"));
+
     }
 
     private static void invokePrePersist(final HsOfficeMembershipEntity membershipEntity)
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java
index 343325a..100a411 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java
@@ -134,6 +134,7 @@
                                    "debitorUuid": "%s",
                                    "bankAccountUuid": "%s",
                                    "reference": "temp ref CAT A",
+                                   "agreement": "2020-01-02",
                                    "validFrom": "2022-10-13"
                                  }
                             """.formatted(givenDebitor.getUuid(), givenBankAccount.getUuid()))
@@ -200,6 +201,7 @@
                                    "debitorUuid": "%s",
                                    "bankAccountUuid": "%s",
                                    "reference": "temp ref CAT C",
+                                   "agreement": "2022-10-12",
                                    "validFrom": "2022-10-13",
                                    "validTo": "2024-12-31"
                                  }
@@ -229,6 +231,7 @@
                                    "debitorUuid": "%s",
                                    "bankAccountUuid": "%s",
                                    "reference": "temp refCAT D",
+                                   "agreement": "2022-10-12",
                                    "validFrom": "2022-10-13",
                                    "validTo": "2024-12-31"
                                  }
@@ -403,7 +406,7 @@
             // finally, the sepaMandate is actually updated
             assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isPresent().get()
                     .matches(mandate -> {
-                        assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,)");
+                        assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2023-03-31)");
                         return true;
                     });
         }
@@ -480,6 +483,7 @@
                     .debitor(givenDebitor)
                     .bankAccount(givenBankAccount)
                     .reference("temp ref CAT Z")
+                    .agreement(LocalDate.parse("2022-10-31"))
                     .validity(Range.closedOpen(
                             LocalDate.parse("2022-11-01"), LocalDate.parse("2023-03-31")))
                     .build();
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityPatcherUnitTest.java
new file mode 100644
index 0000000..7b025ec
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityPatcherUnitTest.java
@@ -0,0 +1,98 @@
+package net.hostsharing.hsadminng.hs.office.sepamandate;
+
+import com.vladmihalcea.hibernate.type.range.Range;
+import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
+import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandatePatchResource;
+import net.hostsharing.test.PatchUnitTestBase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.persistence.EntityManager;
+import java.time.LocalDate;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import static net.hostsharing.hsadminng.hs.office.bankaccount.TestHsOfficeBankAccount.TEST_BANK_ACCOUNT;
+import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR;
+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;
+
+@TestInstance(PER_CLASS)
+@ExtendWith(MockitoExtension.class)
+class HsOfficeSepaMandateEntityPatcherUnitTest extends PatchUnitTestBase<
+        HsOfficeSepaMandatePatchResource,
+        HsOfficeSepaMandateEntity
+        > {
+
+    private static final UUID INITIAL_SepaMandate_UUID = UUID.randomUUID();
+    private static final LocalDate GIVEN_VALID_FROM = LocalDate.parse("2020-04-15");
+    private static final LocalDate PATCHED_VALID_FROM = LocalDate.parse("2022-10-30");
+    private static final LocalDate PATCHED_VALID_TO = LocalDate.parse("2022-12-31");
+    private static final LocalDate PATCHED_AGREEMENT = LocalDate.parse("2022-11-01");
+    private static final String PATCHED_REFERENCE = "ref sepamandate-patched";
+
+    @Mock
+    private EntityManager em;
+
+    @BeforeEach
+    void initMocks() {
+        lenient().when(em.getReference(eq(HsOfficeDebitorEntity.class), any())).thenAnswer(invocation ->
+                HsOfficeDebitorEntity.builder().uuid(invocation.getArgument(1)).build());
+        lenient().when(em.getReference(eq(HsOfficeSepaMandateEntity.class), any())).thenAnswer(invocation ->
+                HsOfficeSepaMandateEntity.builder().uuid(invocation.getArgument(1)).build());
+    }
+
+    @Override
+    protected HsOfficeSepaMandateEntity newInitialEntity() {
+        final var entity = new HsOfficeSepaMandateEntity();
+        entity.setUuid(INITIAL_SepaMandate_UUID);
+        entity.setDebitor(TEST_DEBITOR);
+        entity.setBankAccount(TEST_BANK_ACCOUNT);
+        entity.setReference("ref sepamandate");
+        entity.setAgreement(LocalDate.parse("2022-10-28"));
+        entity.setValidity(Range.closedInfinite(GIVEN_VALID_FROM));
+        return entity;
+    }
+
+    @Override
+    protected HsOfficeSepaMandatePatchResource newPatchResource() {
+        return new HsOfficeSepaMandatePatchResource();
+    }
+
+    @Override
+    protected HsOfficeSepaMandateEntityPatcher createPatcher(final HsOfficeSepaMandateEntity sepaMandate) {
+        return new HsOfficeSepaMandateEntityPatcher(sepaMandate);
+    }
+
+    @Override
+    protected Stream<Property> propertyTestDescriptors() {
+        return Stream.of(
+                new JsonNullableProperty<>(
+                        "reference",
+                        HsOfficeSepaMandatePatchResource::setReference,
+                        PATCHED_REFERENCE,
+                        HsOfficeSepaMandateEntity::setReference),
+
+                new JsonNullableProperty<>(
+                        "agreement",
+                        HsOfficeSepaMandatePatchResource::setAgreement,
+                        PATCHED_AGREEMENT,
+                        HsOfficeSepaMandateEntity::setAgreement),
+                new JsonNullableProperty<>(
+                        "validfrom",
+                        HsOfficeSepaMandatePatchResource::setValidFrom,
+                        PATCHED_VALID_FROM,
+                        HsOfficeSepaMandateEntity::setValidFrom),
+                new JsonNullableProperty<>(
+                        "validto",
+                        HsOfficeSepaMandatePatchResource::setValidTo,
+                        PATCHED_VALID_TO,
+                        HsOfficeSepaMandateEntity::setValidTo)
+        );
+    }
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityUnitTest.java
index 02dee97..7ba77e0 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityUnitTest.java
@@ -7,14 +7,17 @@
 import java.time.LocalDate;
 
 import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR;
+import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
 import static org.assertj.core.api.Assertions.assertThat;
 
 class HsOfficeSepaMandateEntityUnitTest {
+    public static final LocalDate GIVEN_VALID_FROM = LocalDate.parse("2020-01-01");
+    public static final LocalDate GIVEN_VALID_TO = LocalDate.parse("2030-12-31");
 
     final HsOfficeSepaMandateEntity givenSepaMandate = HsOfficeSepaMandateEntity.builder()
             .debitor(TEST_DEBITOR)
             .reference("some-ref")
-            .validity(Range.closedOpen(LocalDate.parse("2020-01-01"), LocalDate.parse("2031-01-01")))
+            .validity(toPostgresDateRange(GIVEN_VALID_FROM, GIVEN_VALID_TO))
             .bankAccount(HsOfficeBankAccountEntity.builder().iban("some label").build())
             .build();
 
@@ -31,4 +34,20 @@
 
         assertThat(result).isEqualTo("some-ref");
     }
+
+    @Test
+    void settingValidFromKeepsValidTo() {
+        givenSepaMandate.setValidFrom(LocalDate.parse("2023-12-31"));
+        assertThat(givenSepaMandate.getValidFrom()).isEqualTo(LocalDate.parse("2023-12-31"));
+        assertThat(givenSepaMandate.getValidTo()).isEqualTo(GIVEN_VALID_TO);
+
+    }
+
+    @Test
+    void settingValidToKeepsValidFrom() {
+        givenSepaMandate.setValidTo(LocalDate.parse("2024-12-31"));
+        assertThat(givenSepaMandate.getValidFrom()).isEqualTo(GIVEN_VALID_FROM);
+        assertThat(givenSepaMandate.getValidTo()).isEqualTo(LocalDate.parse("2024-12-31"));
+    }
+
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java
index 818c7bc..98d5ecb 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java
@@ -78,6 +78,7 @@
                         .debitor(givenDebitor)
                         .bankAccount(givenBankAccount)
                         .reference("temp ref A")
+                        .agreement(LocalDate.parse( "2020-01-02"))
                         .validity(Range.closedOpen(
                                 LocalDate.parse("2020-01-01"), LocalDate.parse("2023-01-01")))
                         .build();
@@ -110,6 +111,7 @@
                         .debitor(givenDebitor)
                         .bankAccount(givenBankAccount)
                         .reference("temp ref B")
+                        .agreement(LocalDate.parse("2020-01-02"))
                         .validity(Range.closedOpen(
                                 LocalDate.parse("2020-01-01"), LocalDate.parse("2023-01-01")))
                         .build();
@@ -178,9 +180,9 @@
             // then
             allTheseSepaMandatesAreReturned(
                     result,
-                    "SEPA-Mandate(DE02120300000000202051, refFirstGmbH, [2022-10-01,2027-01-01))",
-                    "SEPA-Mandate(DE02100500000054540402, refSeconde.K., [2022-10-01,2027-01-01))",
-                    "SEPA-Mandate(DE02300209000106531065, refThirdOHG, [2022-10-01,2027-01-01))");
+                    "SEPA-Mandate(DE02120300000000202051, refFirstGmbH, 2022-09-30, [2022-10-01,2027-01-01))",
+                    "SEPA-Mandate(DE02100500000054540402, refSeconde.K., 2022-09-30, [2022-10-01,2027-01-01))",
+                    "SEPA-Mandate(DE02300209000106531065, refThirdOHG, 2022-09-30, [2022-10-01,2027-01-01))");
         }
 
         @Test
@@ -194,7 +196,7 @@
             // then:
             exactlyTheseSepaMandatesAreReturned(
                     result,
-                    "SEPA-Mandate(DE02120300000000202051, refFirstGmbH, [2022-10-01,2027-01-01))");
+                    "SEPA-Mandate(DE02120300000000202051, refFirstGmbH, 2022-09-30, [2022-10-01,2027-01-01))");
         }
     }
 
@@ -212,9 +214,9 @@
             // then
             exactlyTheseSepaMandatesAreReturned(
                     result,
-                    "SEPA-Mandate(DE02120300000000202051, refFirstGmbH, [2022-10-01,2027-01-01))",
-                    "SEPA-Mandate(DE02100500000054540402, refSeconde.K., [2022-10-01,2027-01-01))",
-                    "SEPA-Mandate(DE02300209000106531065, refThirdOHG, [2022-10-01,2027-01-01))");
+                    "SEPA-Mandate(DE02120300000000202051, refFirstGmbH, 2022-09-30, [2022-10-01,2027-01-01))",
+                    "SEPA-Mandate(DE02100500000054540402, refSeconde.K., 2022-09-30, [2022-10-01,2027-01-01))",
+                    "SEPA-Mandate(DE02300209000106531065, refThirdOHG, 2022-09-30, [2022-10-01,2027-01-01))");
         }
 
         @Test
@@ -228,7 +230,7 @@
             // then
             exactlyTheseSepaMandatesAreReturned(
                     result,
-                    "SEPA-Mandate(DE02300209000106531065, refThirdOHG, [2022-10-01,2027-01-01))");
+                    "SEPA-Mandate(DE02300209000106531065, refThirdOHG, 2022-09-30, [2022-10-01,2027-01-01))");
         }
     }
 
@@ -420,6 +422,7 @@
                     .debitor(givenDebitor)
                     .bankAccount(givenBankAccount)
                     .reference("temp ref X")
+                    .agreement(LocalDate.parse( "2020-01-02"))
                     .validity(Range.closedOpen(
                             LocalDate.parse("2020-01-01"), LocalDate.parse("2023-01-01")))
                     .build();

--
Gitblit v1.9.3