diff options
Diffstat (limited to 'modules')
6 files changed, 232 insertions, 40 deletions
| diff --git a/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java b/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java index 4212aae8..5d3f43e6 100644 --- a/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java +++ b/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java @@ -14,12 +14,14 @@ import java.util.Objects;  import java.util.Optional;  import java.util.Set;  import java.util.stream.Collectors; +import java.util.stream.Stream;  import javax.annotation.Nonnull;  import javax.annotation.PostConstruct;  import org.apache.commons.io.IOUtils;  import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair;  import org.apache.http.client.HttpClient;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.http.MediaType; @@ -40,6 +42,7 @@ import com.fasterxml.jackson.databind.SerializationFeature;  import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants; +import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ErnpPersonRegisterResult;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.RegisterResult;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.SimpleEidasData;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.SimpleEidasData.SimpleEidasDataBuilder; @@ -78,7 +81,6 @@ import at.gv.egiz.eaaf.core.impl.http.HttpClientConfiguration;  import at.gv.egiz.eaaf.core.impl.http.HttpClientConfiguration.ClientAuthMode;  import at.gv.egiz.eaaf.core.impl.http.IHttpClientFactory;  import at.gv.egiz.eaaf.core.impl.utils.TransactionIdUtils; -import lombok.AllArgsConstructor;  import lombok.Builder;  import lombok.Getter;  import lombok.extern.slf4j.Slf4j; @@ -120,6 +122,9 @@ public class ErnpRestClient implements IErnpClient {    // HTTP header-names from ERnP response     private static final String ERNP_RESPONSE_HEADER_SERVER_ID = "Server-Request-Id"; +  // ERnP person type that indicates mark a person as ZMR entry +  private static final String ERNP_RESPONSE_OPERATION_ZMR_FORWARD = "PersonUebernehmen"; +      @Autowired    IConfiguration basicConfig; @@ -265,7 +270,7 @@ public class ErnpRestClient implements IErnpClient {        if (eidasDocumentToAdd.isEmpty() && mdsToUpdate == null) {          log.info("Find no eIDAS document or MDS for update during: {}. Nothing todo on ERnP side",               PROCESS_KITT_GENERAL); -        return new ErnpRegisterResult(Arrays.asList(registerResult)); +        return new ErnpRegisterResult(Arrays.asList(new ErnpPersonRegisterResult(registerResult, false)));        } else {          log.info("Find #{} eIDAS documents for update during: {}", eidasDocumentToAdd.size(), PROCESS_KITT_GENERAL); @@ -386,30 +391,32 @@ public class ErnpRestClient implements IErnpClient {            resp.getPerson().size(), processStepFiendlyname);        if (forceSinglePersonMatch) { -        return new ErnpRegisterResult(processSearchPersonResponseSingleResult( -            resp.getPerson(), citizenCountryCode, processStepFiendlyname)); +        return processSearchPersonResponseSingleResult( +            resp.getPerson(), citizenCountryCode, processStepFiendlyname);        } else { -        return new ErnpRegisterResult(processSearchPersonResponse( -            resp.getPerson(), citizenCountryCode)); +        return processSearchPersonResponse( +            resp.getPerson(), citizenCountryCode);        }      }    }    @Nonnull -  private List<RegisterResult> processSearchPersonResponse( +  private ErnpRegisterResult processSearchPersonResponse(        @Nonnull List<Person> list,        @Nonnull String citizenCountryCode) throws EaafAuthenticationException { -    return list.stream() +    List<ErnpPersonRegisterResult> ernpResult = list.stream()          .map(el -> mapErnpResponseToRegisterResult(el, citizenCountryCode))          .filter(Objects::nonNull)          .collect(Collectors.toList()); +    log.info("Get #{} ERnP results after post-processing", ernpResult.size());           +    return new ErnpRegisterResult(ernpResult);    }    @NonNull -  private List<RegisterResult> processSearchPersonResponseSingleResult( +  private ErnpRegisterResult processSearchPersonResponseSingleResult(        @Nonnull List<Person> persons,        @Nonnull String citizenCountryCode, String processStepFiendlyname) throws EaafAuthenticationException {      if (persons.size() > 1) { @@ -418,7 +425,8 @@ public class ErnpRestClient implements IErnpClient {            "Find more-than-one ERnP entry with search criteria that has to be unique", true);      } else { -      RegisterResult activeResult = mapErnpResponseToRegisterResult(persons.get(0), citizenCountryCode); +      ErnpPersonRegisterResult activeResult =  +          mapErnpResponseToRegisterResult(persons.get(0), citizenCountryCode);        if (activeResult == null) {          log.error("ERnP entry, which was selected by matching, looks already closed. "              + "Automated operations on closed entries not supported my matching"); @@ -426,40 +434,41 @@ public class ErnpRestClient implements IErnpClient {              "ERnP entry, which was selected by matching, is not active any more.", true);        }       -             -      return Arrays.asList(activeResult); - +       +      return new ErnpRegisterResult(Arrays.asList(activeResult)); +            }    } -   -      /**     * Process a single Person data-set from ERnP.      *      * @param personEl Person data-set from ERnP     * @param citizenCountryCode Country-Code of the citizen -   * @return Simplified register result, or <code>null</code> if the person data-set is not active anymore +   * @return {@link Pair} of Simplified register result and 'isZMREntry' flag,  +   *    or <code>null</code> if the person data-set is not active anymore     * @throws EaafAuthenticationException In case of a validation error     */    @Nullable -  private RegisterResult mapErnpResponseToRegisterResult(@Nonnull Person person, +  private ErnpPersonRegisterResult mapErnpResponseToRegisterResult(@Nonnull Person person,        @Nonnull String citizenCountryCode) {      if (checkIfPersonIsActive(person)) {            // build result -      return RegisterResult.builder() -          .pseudonym(selectAllEidasDocument(person, citizenCountryCode, -              EidasConstants.eIDAS_ATTRURN_PERSONALIDENTIFIER)) -          .familyName(person.getPersonendaten().getFamilienname()) -          .givenName(person.getPersonendaten().getVorname()) -          .dateOfBirth(getTextualBirthday(person.getPersonendaten().getGeburtsdatum())) -          .bpk(person.getPersonendaten().getBpkZp()) -          .placeOfBirth(selectSingleEidasDocument(person, citizenCountryCode, -              EidasConstants.eIDAS_ATTRURN_PLACEOFBIRTH)) -          .birthName(selectSingleEidasDocument(person, citizenCountryCode, -              EidasConstants.eIDAS_ATTRURN_BIRTHNAME)) -          .build(); +      return new ErnpPersonRegisterResult( +          RegisterResult.builder() +            .pseudonym(selectAllEidasDocument(person, citizenCountryCode, +                EidasConstants.eIDAS_ATTRURN_PERSONALIDENTIFIER)) +            .familyName(person.getPersonendaten().getFamilienname()) +            .givenName(person.getPersonendaten().getVorname()) +            .dateOfBirth(getTextualBirthday(person.getPersonendaten().getGeburtsdatum())) +            .bpk(person.getPersonendaten().getBpkZp()) +            .placeOfBirth(selectSingleEidasDocument(person, citizenCountryCode, +                EidasConstants.eIDAS_ATTRURN_PLACEOFBIRTH)) +            .birthName(selectSingleEidasDocument(person, citizenCountryCode, +                EidasConstants.eIDAS_ATTRURN_BIRTHNAME)) +            .build(), +          isPersonMovedToZmr(person));                          } else {        log.debug("Entity is not valid anymore. Skip it ... "); @@ -473,7 +482,12 @@ public class ErnpRestClient implements IErnpClient {      if (person.getGueltigBis() != null) {        LocalDateTime validTo = person.getGueltigBis().toLocalDateTime();        LocalDateTime now = LocalDateTime.now(); -      if (validTo.isBefore(now)) { +       +      if (isPersonMovedToZmr(person)) { +        log.debug("Entity has a 'validTo' element, but it's marked as {}. Use it as a ZMR entry",  +            ERNP_RESPONSE_OPERATION_ZMR_FORWARD);         +         +      } else if (validTo.isBefore(now)) {                                  log.warn("Enity was valid to: {}, but now its: {}. Ignore that entry", validTo, now);          return false; @@ -491,6 +505,20 @@ public class ErnpRestClient implements IErnpClient {    } +  /** +   * Check if ERnP person is marked as KITT to ZMR entry. +   *  +   * <p>If person is marked as ZMR person then it has the same quality as a ZMR match.</p> +   *  +   * @param person ERnP person result +   * @return <code>true</code> if the person should be in ERnP, otherwise <code>false</code> +   */ +  private boolean isPersonMovedToZmr(Person person) { +    return person.getLetzteOperation() != null  +        && ERNP_RESPONSE_OPERATION_ZMR_FORWARD.equals(person.getLetzteOperation().getVorgang()); +     +  } +    private Suchdaten mapCountrySpecificSearchData(PersonSuchenRequest personSearchDao) {                  final Suchdaten searchInfos = new Suchdaten();      searchInfos.setFamilienname(personSearchDao.getNatuerlichePerson().getPersonenName().getFamilienname());       @@ -541,8 +569,8 @@ public class ErnpRestClient implements IErnpClient {          generic.getClientRequestTime(), generic.getClientRequestId(), ernpReq);              log.trace("Receive response from ERnP for '{}' operation", PROCESS_KITT_IDENITIES_UPDATE); -    return  new ErnpRegisterResult(Arrays.asList( -        mapErnpResponseToRegisterResult(ernpResp.getPerson(), citizenCountryCode)));     +    return new ErnpRegisterResult(Collections.singletonList( +        mapErnpResponseToRegisterResult(ernpResp.getPerson(), citizenCountryCode)));    } @@ -896,11 +924,64 @@ public class ErnpRestClient implements IErnpClient {      return config;    } -  @AllArgsConstructor    @Getter    public static class ErnpRegisterResult { -    private final List<RegisterResult> personResult; +    private List<ErnpPersonRegisterResult> fullErnpResults; +     +    /** +     * Build reduced ERnP register result. +     * @param list {@link List} of ERnP entities  +     */ +    public ErnpRegisterResult(List<ErnpPersonRegisterResult> list) { +      fullErnpResults = list; +       +    } +     +    /** +     * Get all active ERnP results. +     *  +     * @return ERnP entities +     */ +    public Stream<ErnpPersonRegisterResult> getPersonResultStream() { +     return fullErnpResults.stream() +         .filter(el -> !el.isZmrEntryNow()); +          +    } +     +    /** +     * Get all active ERnP results. +     *  +     * @return ERnP entities +     */ +    public List<RegisterResult> getPersonResult() { +     return getPersonResultStream() +         .collect(Collectors.toList());   +          +    } + +    /** +     * Get all ERnP results that are kitted to ZMR entries. +     *  +     * @return entities that are in ZMR now +     */ +    public Stream<ErnpPersonRegisterResult> getZmrPersonResultStream() { +      return fullErnpResults.stream() +          .filter(el -> el.isZmrEntryNow()); +       +    } +     +    /** +     * Get all ERnP results that are kitted to ZMR entries. +     *  +     * @return entities that are in ZMR now +     */ +    public List<RegisterResult> getZmrPersonResult() { +      return getZmrPersonResultStream() +          .collect(Collectors.toList()); +       +    } +        }    private GenericRequestParams buildGenericRequestParameters() { diff --git a/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/dao/ErnpPersonRegisterResult.java b/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/dao/ErnpPersonRegisterResult.java new file mode 100644 index 00000000..1b9454db --- /dev/null +++ b/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/dao/ErnpPersonRegisterResult.java @@ -0,0 +1,30 @@ +package at.asitplus.eidas.specific.modules.auth.eidas.v2.dao; + +import lombok.Getter; + +/** + * ERnP specific extension of a register result. + *  + * @author tlenz + * + */ +@Getter +public class ErnpPersonRegisterResult extends RegisterResult { + +  private static final long serialVersionUID = -1250543763613825226L; + +  /** +   * <code>true</code> in case of person is still found in ERnP, but is in ZMR now.   +   */ +  private final boolean zmrEntryNow; +   +  public ErnpPersonRegisterResult(RegisterResult result, boolean isZmrEntryNow) { +    super(result.getPseudonym(), result.getGivenName(), result.getFamilyName(), result.getDateOfBirth(),  +        result.getPlaceOfBirth(), result.getBirthName(), result.getTaxNumber(), result.getAddress(),  +        result.getBpk()); +     +    zmrEntryNow = isZmrEntryNow; +     +  } + +} diff --git a/modules/authmodule-eIDAS-v2/src/test/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/test/clients/ErnpRestClientTest.java b/modules/authmodule-eIDAS-v2/src/test/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/test/clients/ErnpRestClientTest.java index 93d8ab74..82d89e3e 100644 --- a/modules/authmodule-eIDAS-v2/src/test/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/test/clients/ErnpRestClientTest.java +++ b/modules/authmodule-eIDAS-v2/src/test/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/test/clients/ErnpRestClientTest.java @@ -163,6 +163,7 @@ public class ErnpRestClientTest {      ErnpRegisterResult resp = client.searchWithResidenceData(null, null, null, null, null, null);                 assertNotNull("no ERnP response", resp);      assertEquals("wrong resp size", 0, resp.getPersonResult().size()); +    assertEquals("wrong resp size", 0, resp.getZmrPersonResult().size());    } @@ -290,6 +291,7 @@ public class ErnpRestClientTest {      mockWebServer.takeRequest();      assertNotNull("no ERnP response", resp);      assertEquals("wrong resp size", 2, resp.getPersonResult().size()); +    assertEquals("wrong resp size", 0, resp.getZmrPersonResult().size());    } @@ -316,9 +318,7 @@ public class ErnpRestClientTest {      assertEquals("wrong resp size", 2, resp.getPersonResult().size());    } -   -   -   +          @Test    @SneakyThrows    public void searchWithPersonalIdNoResponse() { @@ -432,7 +432,33 @@ public class ErnpRestClientTest {      mockWebServer.takeRequest();      assertNotNull("no ERnP response", resp);      assertEquals("wrong resp size", 1, resp.getPersonResult().size());    +    assertEquals("wrong resp size", 0, resp.getZmrPersonResult().size()); +         +  } +   +  @Test +  @SneakyThrows +  public void searchWithPersonalIdZmrKitt() { +    final String cc = "DE"; +    final SimpleEidasData eidasDataFirst = generateRandomEidasData(cc); +    // set ERnP response +    mockWebServer.enqueue(new MockResponse().setResponseCode(200) +        .setBody(IOUtils.toString( +            ErnpRestClientTest.class.getResourceAsStream( +                "/data/ernp/1_search_with_personalId_zmr_kitt_resp.json"),  +            "UTF-8")) +        .setHeader("Content-Type", "application/json;charset=utf-8")); +         +     // execute operation +    ErnpRegisterResult resp = client.searchWithPersonIdentifier(eidasDataFirst.getPseudonym(), cc); +                 +    // validate state +    mockWebServer.takeRequest(); +    assertNotNull("no ERnP response", resp); +    assertEquals("wrong resp size", 0, resp.getPersonResult().size());    +    assertEquals("wrong resp size", 1, resp.getZmrPersonResult().size()); +            }    @Test @@ -455,6 +481,7 @@ public class ErnpRestClientTest {      mockWebServer.takeRequest();      assertNotNull("no ERnP response", resp);      assertEquals("wrong resp size", 1, resp.getPersonResult().size()); +    assertEquals("wrong resp size", 0, resp.getZmrPersonResult().size());      RegisterResult persInfo = resp.getPersonResult().get(0);      assertEquals("wrong familyname", "CtKKrtUe", persInfo.getFamilyName());      assertEquals("wrong givenName", "dUeYzUFg", persInfo.getGivenName()); @@ -619,6 +646,7 @@ public class ErnpRestClientTest {      mockWebServer.takeRequest();      assertNotNull("no ERnP response", resp);      assertEquals("wrong resp size", 1, resp.getPersonResult().size()); +    assertEquals("wrong resp size", 0, resp.getZmrPersonResult().size());      RegisterResult persInfo = resp.getPersonResult().get(0);      assertEquals("wrong familyname", "CtKKrtUe", persInfo.getFamilyName());      assertEquals("wrong givenName", "dUeYzUFg", persInfo.getGivenName()); diff --git a/modules/authmodule-eIDAS-v2/src/test/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/test/tasks/CreateNewErnpEntryTaskTest.java b/modules/authmodule-eIDAS-v2/src/test/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/test/tasks/CreateNewErnpEntryTaskTest.java index 985a5e14..6298e250 100644 --- a/modules/authmodule-eIDAS-v2/src/test/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/test/tasks/CreateNewErnpEntryTaskTest.java +++ b/modules/authmodule-eIDAS-v2/src/test/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/test/tasks/CreateNewErnpEntryTaskTest.java @@ -9,6 +9,7 @@ import static org.mockito.ArgumentMatchers.any;  import java.net.URISyntaxException;  import java.util.Arrays;  import java.util.List; +import java.util.stream.Collectors;  import org.apache.commons.lang3.RandomStringUtils;  import org.jetbrains.annotations.NotNull; @@ -28,6 +29,7 @@ import org.springframework.web.context.request.ServletRequestAttributes;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.clients.ernp.ErnpRestClient;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.clients.ernp.ErnpRestClient.ErnpRegisterResult; +import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ErnpPersonRegisterResult;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.MatchedPersonResult;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.RegisterResult;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.SimpleEidasData; @@ -150,7 +152,9 @@ public class CreateNewErnpEntryTaskTest {    @NotNull    private ErnpRegisterResult ernpRegisterResult(List<RegisterResult> registerResult) { -    return new ErnpRegisterResult(registerResult); +    return new ErnpRegisterResult(registerResult.stream() +        .map(el -> new ErnpPersonRegisterResult(el, false)) +        .collect(Collectors.toList()));    } diff --git a/modules/authmodule-eIDAS-v2/src/test/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/test/tasks/InitialSearchTaskTest.java b/modules/authmodule-eIDAS-v2/src/test/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/test/tasks/InitialSearchTaskTest.java index c9b7b1ac..4cfba521 100644 --- a/modules/authmodule-eIDAS-v2/src/test/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/test/tasks/InitialSearchTaskTest.java +++ b/modules/authmodule-eIDAS-v2/src/test/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/test/tasks/InitialSearchTaskTest.java @@ -41,6 +41,7 @@ import java.util.Collections;  import java.util.List;  import java.util.Map;  import java.util.Random; +import java.util.stream.Collectors;  import javax.xml.namespace.QName; @@ -68,6 +69,7 @@ import at.asitplus.eidas.specific.modules.auth.eidas.v2.clients.ernp.IErnpClient  import at.asitplus.eidas.specific.modules.auth.eidas.v2.clients.zmr.IZmrClient;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.clients.zmr.ZmrSoapClient;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.clients.zmr.ZmrSoapClient.ZmrRegisterResult; +import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ErnpPersonRegisterResult;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.MatchedPersonResult;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.RegisterResult;  import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.SimpleEidasData; @@ -809,12 +811,15 @@ public class InitialSearchTaskTest {    @NotNull    private ErnpRegisterResult ernpRegisterResult(RegisterResult registerResult) { -    return new ErnpRegisterResult(Collections.singletonList(registerResult)); +    return new ErnpRegisterResult(Collections.singletonList( +        new ErnpPersonRegisterResult(registerResult, false)));    }    @NotNull    private ErnpRegisterResult ernpRegisterResult(List<RegisterResult> registerResult) { -    return new ErnpRegisterResult(registerResult); +    return new ErnpRegisterResult(registerResult.stream() +        .map(el -> new ErnpPersonRegisterResult(el, false)) +        .collect(Collectors.toList()));    }    @NotNull diff --git a/modules/authmodule-eIDAS-v2/src/test/resources/data/ernp/1_search_with_personalId_zmr_kitt_resp.json b/modules/authmodule-eIDAS-v2/src/test/resources/data/ernp/1_search_with_personalId_zmr_kitt_resp.json new file mode 100644 index 00000000..5612e3d0 --- /dev/null +++ b/modules/authmodule-eIDAS-v2/src/test/resources/data/ernp/1_search_with_personalId_zmr_kitt_resp.json @@ -0,0 +1,44 @@ +{ +  "person": [ +    { +      "type": "Person", +      "eidas": [ +        { +          "ablaufDatum": "9999-12-31T00:00:00.000+01:00", +          "art": "http://eidas.europa.eu/attributes/naturalperson/PersonIdentifier", +          "ausstellDatum": "9999-12-31T00:00:00.000+01:00", +          "entityId": "47769100000077607", +          "gueltigAb": "2022-10-06T08:01:18.117+02:00", +          "gueltigBis": "2022-10-06T08:01:18.117+02:00", +          "staatscode2": "XZ", +          "wert": "eidasmatcherclosed19740404_01" +        } +      ], +      "entityId": "47769100000077596", +      "gueltigAb": "2022-10-06T08:01:18.117+02:00", +      "gueltigBis": "2045-10-06T08:01:18.117+02:00", +      "letzteOperation": { +        "begruendung": "EIDAS Integrationstest", +        "grund": "Person amtlich beenden", +        "vorgang": "PersonUebernehmen", +        "zeitpunkt": "2022-10-06T08:01:18.117+02:00" +      }, +      "personendaten": { +        "basiszahl": "000862899079", +        "bpkZp": "mhnWeYYC8KfRY/MaYKdUDkzwD2w=", +        "entityId": "47769100000077596", +        "familienname": "EidasMatcher", +        "geburtsdatum": { +          "jahr": 1974, +          "monat": 4, +          "tag": 4 +        }, +        "geprueft": false, +        "gueltigAb": "2022-10-06T08:01:18.117+02:00", +        "gueltigBis": "2022-10-06T08:01:18.117+02:00", +        "vorname": "Closed" +      }, +      "version": "2022-10-06T08:01:18.117+02:00" +    } +  ] +}
\ No newline at end of file | 
